mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-12 02:43:50 +00:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da829dc09f | |||
| c4d65dbe5f | |||
| 2586b0b10c | |||
| abe27d54a2 | |||
| 1343818917 | |||
| a93cd3e9b1 | |||
| 813eb9ad86 | |||
| 34e9b1ef07 | |||
| a67b89190d | |||
| ecd151a0a8 | |||
| 4cbc09864c | |||
| 20d5d8b129 | |||
| 11db39ee73 | |||
| edb79d5819 | |||
| 715fa66444 | |||
| 27f6d10677 | |||
| f2504072ce | |||
| 9e285ee8f3 | |||
| 1ccf2108eb | |||
| db56ac8049 | |||
| 0660f014c8 | |||
| 41c26778cb | |||
| 804e4ad33e | |||
| b11041938d | |||
| 5be60ff6c3 | |||
| bf8ebe942e | |||
| bdd216d881 | |||
| e6b4f537af | |||
| 797a0edf57 | |||
| c4886a617c | |||
| a79ae7e0c4 | |||
| 8cabb347e9 | |||
| 22d89c6fbc | |||
| facce4f096 | |||
| afa62a6e38 | |||
| 1b55dbc170 | |||
| 2316bbebac | |||
| 884ab0b67b | |||
| 58239af8bb | |||
| c59e070076 |
@@ -28,19 +28,19 @@ jobs:
|
|||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
# Run code quality per package
|
# Run code quality per package
|
||||||
- name: React - Run linter
|
- name: React - Run linter + TypeScript checks
|
||||||
if: always()
|
if: always()
|
||||||
run: pnpm lint
|
run: pnpm lint && tsc --noEmit
|
||||||
working-directory: ./apps/client
|
working-directory: ./apps/client
|
||||||
|
|
||||||
- name: Server - Run linter
|
- name: Server - Run linter + TypeScript checks
|
||||||
if: always()
|
if: always()
|
||||||
run: pnpm lint
|
run: pnpm lint && tsc --noEmit
|
||||||
working-directory: ./apps/server
|
working-directory: ./apps/server
|
||||||
|
|
||||||
- name: Utils - Run linter
|
- name: Utils - Run linter + TypeScript checks
|
||||||
if: always()
|
if: always()
|
||||||
run: pnpm lint
|
run: pnpm lint && tsc --noEmit
|
||||||
working-directory: ./packages/utils
|
working-directory: ./packages/utils
|
||||||
|
|
||||||
- name: Types - Run linter
|
- name: Types - Run linter
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
. "$(dirname -- "$0")/_/husky.sh"
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
pnpm lint
|
pnpm lint-staged
|
||||||
|
|||||||
+1
-1
@@ -76,7 +76,7 @@ You can generate a distribution for your OS by running the following steps.
|
|||||||
From the project root, run the following commands
|
From the project root, run the following commands
|
||||||
|
|
||||||
- __Install the project dependencies__ by running `pnpm i`
|
- __Install the project dependencies__ by running `pnpm i`
|
||||||
- __Build the UI and server__ by running `turbo build:local`
|
- __Build the UI and server__ by running `turbo build:electron`
|
||||||
- __Create the package__ by running `turbo dist-win`, `turbo dist-mac` or `turbo dist-linux`
|
- __Create the package__ by running `turbo dist-win`, `turbo dist-mac` or `turbo dist-linux`
|
||||||
|
|
||||||
The build distribution assets will be at `.apps/electron/dist`
|
The build distribution assets will be at `.apps/electron/dist`
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime-ui",
|
"name": "ontime-ui",
|
||||||
"version": "2.16.2",
|
"version": "2.24.8",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/react": "^2.7.0",
|
"@chakra-ui/react": "^2.7.0",
|
||||||
@@ -12,8 +12,8 @@
|
|||||||
"@react-icons/all-files": "^4.1.0",
|
"@react-icons/all-files": "^4.1.0",
|
||||||
"@sentry/react": "^7.46.0",
|
"@sentry/react": "^7.46.0",
|
||||||
"@sentry/tracing": "^7.46.0",
|
"@sentry/tracing": "^7.46.0",
|
||||||
"@tanstack/react-query": "^4.28.0",
|
"@tanstack/react-query": "^5.8.4",
|
||||||
"@tanstack/react-query-devtools": "^4.29.0",
|
"@tanstack/react-query-devtools": "^5.8.4",
|
||||||
"@tanstack/react-table": "^8.9.2",
|
"@tanstack/react-table": "^8.9.2",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
"axios": "^1.2.0",
|
"axios": "^1.2.0",
|
||||||
@@ -38,8 +38,10 @@
|
|||||||
"dev": "cross-env BROWSER=none vite",
|
"dev": "cross-env BROWSER=none vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build:local": "cross-env NODE_ENV=local vite build",
|
"build:local": "cross-env NODE_ENV=local vite build",
|
||||||
|
"build:electron": "cross-env NODE_ENV=local vite build",
|
||||||
"build:docker": "vite build",
|
"build:docker": "vite build",
|
||||||
"lint": "eslint . --quiet",
|
"lint": "eslint . --quiet",
|
||||||
|
"lint-staged": "eslint",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:pipeline": "vitest run",
|
"test:pipeline": "vitest run",
|
||||||
"cleanup": "rm -rf .turbo && rm -rf node_modules && rm -rf build"
|
"cleanup": "rm -rf .turbo && rm -rf node_modules && rm -rf build"
|
||||||
@@ -58,7 +60,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sentry/vite-plugin": "^0.4.0",
|
"@sentry/vite-plugin": "^0.4.0",
|
||||||
"@tanstack/eslint-plugin-query": "^4.26.2",
|
"@tanstack/eslint-plugin-query": "^5.8.4",
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@testing-library/react": "^13.1.1",
|
"@testing-library/react": "^13.1.1",
|
||||||
"@testing-library/user-event": "^14.1.1",
|
"@testing-library/user-event": "^14.1.1",
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
export const PROJECT_DATA = ['project'];
|
export const PROJECT_DATA = ['project'];
|
||||||
export const ALIASES = ['aliases'];
|
export const ALIASES = ['aliases'];
|
||||||
export const USERFIELDS = ['userFields'];
|
export const USERFIELDS = ['userFields'];
|
||||||
export const RUNDOWN_TABLE_KEY = 'rundown';
|
export const RUNDOWN = ['rundown'];
|
||||||
export const RUNDOWN_TABLE = [RUNDOWN_TABLE_KEY];
|
|
||||||
export const APP_INFO = ['appinfo'];
|
export const APP_INFO = ['appinfo'];
|
||||||
export const OSC_SETTINGS = ['oscSettings'];
|
export const OSC_SETTINGS = ['oscSettings'];
|
||||||
|
export const HTTP_SETTINGS = ['httpSettings'];
|
||||||
export const APP_SETTINGS = ['appSettings'];
|
export const APP_SETTINGS = ['appSettings'];
|
||||||
export const VIEW_SETTINGS = ['viewSettings'];
|
export const VIEW_SETTINGS = ['viewSettings'];
|
||||||
export const RUNTIME = ['runtimeStore'];
|
export const RUNTIME = ['runtimeStore'];
|
||||||
|
|||||||
@@ -42,12 +42,12 @@ export function logAxiosError(prepend: string, error: unknown) {
|
|||||||
* Utility function invalidates react-query caches
|
* Utility function invalidates react-query caches
|
||||||
*/
|
*/
|
||||||
export async function invalidateAllCaches() {
|
export async function invalidateAllCaches() {
|
||||||
await ontimeQueryClient.invalidateQueries(['project']);
|
await ontimeQueryClient.invalidateQueries({ queryKey: ['project'] });
|
||||||
await ontimeQueryClient.invalidateQueries(['aliases']);
|
await ontimeQueryClient.invalidateQueries({ queryKey: ['aliases'] });
|
||||||
await ontimeQueryClient.invalidateQueries(['userFields']);
|
await ontimeQueryClient.invalidateQueries({ queryKey: ['userFields'] });
|
||||||
await ontimeQueryClient.invalidateQueries(['rundown']);
|
await ontimeQueryClient.invalidateQueries({ queryKey: ['rundown'] });
|
||||||
await ontimeQueryClient.invalidateQueries(['appinfo']);
|
await ontimeQueryClient.invalidateQueries({ queryKey: ['appinfo'] });
|
||||||
await ontimeQueryClient.invalidateQueries(['oscSettings']);
|
await ontimeQueryClient.invalidateQueries({ queryKey: ['oscSettings'] });
|
||||||
await ontimeQueryClient.invalidateQueries(['appSettings']);
|
await ontimeQueryClient.invalidateQueries({ queryKey: ['appSettings'] });
|
||||||
await ontimeQueryClient.invalidateQueries(['viewSettings']);
|
await ontimeQueryClient.invalidateQueries({ queryKey: ['viewSettings'] });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { OntimeRundown, OntimeRundownEntry } from 'ontime-types';
|
import { GetRundownCached, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
|
||||||
|
|
||||||
import { rundownURL } from './apiConstants';
|
import { rundownURL } from './apiConstants';
|
||||||
|
|
||||||
@@ -7,6 +7,16 @@ import { rundownURL } from './apiConstants';
|
|||||||
* @description HTTP request to fetch all events
|
* @description HTTP request to fetch all events
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
|
export async function fetchCachedRundown(): Promise<GetRundownCached> {
|
||||||
|
const res = await axios.get(`${rundownURL}/cached`);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use fetchCachedRundown instead
|
||||||
|
* @description HTTP request to fetch all events
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
export async function fetchRundown(): Promise<OntimeRundown> {
|
export async function fetchRundown(): Promise<OntimeRundown> {
|
||||||
const res = await axios.get(rundownURL);
|
const res = await axios.get(rundownURL);
|
||||||
return res.data;
|
return res.data;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
Alias,
|
Alias,
|
||||||
DatabaseModel,
|
DatabaseModel,
|
||||||
GetInfo,
|
GetInfo,
|
||||||
|
HttpSettings,
|
||||||
OntimeRundown,
|
OntimeRundown,
|
||||||
OSCSettings,
|
OSCSettings,
|
||||||
OscSubscription,
|
OscSubscription,
|
||||||
@@ -104,6 +105,23 @@ export async function getOSC(): Promise<OSCSettings> {
|
|||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description HTTP request to retrieve http settings
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
export async function getHTTP(): Promise<HttpSettings> {
|
||||||
|
const res = await axios.get(`${ontimeURL}/http`);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description HTTP request to mutate http settings
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
export async function postHTTP(data: HttpSettings) {
|
||||||
|
return axios.post(`${ontimeURL}/http`, data);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description HTTP request to mutate osc settings
|
* @description HTTP request to mutate osc settings
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
|
|||||||
@@ -19,9 +19,11 @@ interface TextInputProps extends BaseProps {
|
|||||||
isTextArea?: false;
|
isTextArea?: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ResizeOptions = 'horizontal' | 'vertical' | 'none';
|
||||||
|
|
||||||
interface TextAreaProps extends BaseProps {
|
interface TextAreaProps extends BaseProps {
|
||||||
isTextArea: true;
|
isTextArea: true;
|
||||||
resize?: 'horizontal' | 'vertical' | 'none';
|
resize?: ResizeOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
type InputProps = TextInputProps | TextAreaProps;
|
type InputProps = TextInputProps | TextAreaProps;
|
||||||
@@ -35,7 +37,7 @@ export default function TextInput(props: InputProps) {
|
|||||||
const textInputProps = useReactiveTextInput(initialText, submitCallback, { submitOnEnter: true });
|
const textInputProps = useReactiveTextInput(initialText, submitCallback, { submitOnEnter: true });
|
||||||
const textAreaProps = useReactiveTextInput(initialText, submitCallback);
|
const textAreaProps = useReactiveTextInput(initialText, submitCallback);
|
||||||
|
|
||||||
let resize = 'none';
|
let resize: ResizeOptions = 'none';
|
||||||
if (isTextArea) {
|
if (isTextArea) {
|
||||||
resize = (props as TextAreaProps)?.resize ?? 'none';
|
resize = (props as TextAreaProps)?.resize ?? 'none';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,6 +212,7 @@ export default function TimeInput(props: TimeInputProps) {
|
|||||||
onKeyDown={onKeyDownHandler}
|
onKeyDown={onKeyDownHandler}
|
||||||
value={value}
|
value={value}
|
||||||
maxLength={8}
|
maxLength={8}
|
||||||
|
autoComplete='off'
|
||||||
/>
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ $progress-bar-br: 3px;
|
|||||||
border-radius: $progress-bar-br;
|
border-radius: $progress-bar-br;
|
||||||
background-color: var(--timer-progress-bg-override, $viewer-card-bg-color);
|
background-color: var(--timer-progress-bg-override, $viewer-card-bg-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
&--hidden {
|
&--hidden {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -31,7 +32,6 @@ $progress-bar-br: 3px;
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
height: inherit;
|
height: inherit;
|
||||||
right: 0;
|
right: 0;
|
||||||
border-radius: $progress-bar-br;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,12 +39,10 @@ $progress-bar-br: 3px;
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
height: inherit;
|
height: inherit;
|
||||||
right: 0;
|
right: 0;
|
||||||
border-radius: 0 $progress-bar-br $progress-bar-br 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.multiprogress-bar__bg-danger {
|
.multiprogress-bar__bg-danger {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: inherit;
|
height: inherit;
|
||||||
right: 0;
|
right: 0;
|
||||||
border-radius: 0 $progress-bar-br $progress-bar-br 0;
|
|
||||||
}
|
}
|
||||||
+16
-12
@@ -3,7 +3,7 @@ import { clamp } from '../../utils/math';
|
|||||||
import './MultiPartProgressBar.scss';
|
import './MultiPartProgressBar.scss';
|
||||||
|
|
||||||
interface MultiPartProgressBar {
|
interface MultiPartProgressBar {
|
||||||
now: number;
|
now: number | null;
|
||||||
complete: number;
|
complete: number;
|
||||||
normalColor: string;
|
normalColor: string;
|
||||||
warning: number;
|
warning: number;
|
||||||
@@ -17,23 +17,27 @@ interface MultiPartProgressBar {
|
|||||||
export default function MultiPartProgressBar(props: MultiPartProgressBar) {
|
export default function MultiPartProgressBar(props: MultiPartProgressBar) {
|
||||||
const { now, complete, normalColor, warning, warningColor, danger, dangerColor, hidden, className = '' } = props;
|
const { now, complete, normalColor, warning, warningColor, danger, dangerColor, hidden, className = '' } = props;
|
||||||
|
|
||||||
const percentComplete = 100 - clamp(100 - (Math.max(now, 0) * 100) / complete, 0, 100);
|
const percentComplete = 100 - clamp(100 - (Math.max(now ?? 0, 0) * 100) / complete, 0, 100);
|
||||||
|
|
||||||
const dangerWidth = clamp((danger / complete) * 100, 0, 100);
|
const dangerWidth = clamp((danger / complete) * 100, 0, 100);
|
||||||
const warningWidth = clamp((warning / complete) * 100, 0, 100);
|
const warningWidth = clamp((warning / complete) * 100, 0, 100);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`multiprogress-bar ${hidden ? 'multiprogress-bar--hidden' : ''} ${className}`}>
|
<div className={`multiprogress-bar ${hidden ? 'multiprogress-bar--hidden' : ''} ${className}`}>
|
||||||
<div className='multiprogress-bar__bg-normal' style={{ backgroundColor: normalColor }} />
|
{now !== null && (
|
||||||
<div
|
<>
|
||||||
className='multiprogress-bar__bg-warning'
|
<div className='multiprogress-bar__bg-normal' style={{ backgroundColor: normalColor }} />
|
||||||
style={{ width: `${warningWidth}%`, backgroundColor: warningColor }}
|
<div
|
||||||
/>
|
className='multiprogress-bar__bg-warning'
|
||||||
<div
|
style={{ width: `${warningWidth}%`, backgroundColor: warningColor }}
|
||||||
className='multiprogress-bar__bg-danger'
|
/>
|
||||||
style={{ width: `${dangerWidth}%`, backgroundColor: dangerColor }}
|
<div
|
||||||
/>
|
className='multiprogress-bar__bg-danger'
|
||||||
<div className='multiprogress-bar__indicator' style={{ width: `${percentComplete}%` }} />
|
style={{ width: `${dangerWidth}%`, backgroundColor: dangerColor }}
|
||||||
|
/>
|
||||||
|
<div className='multiprogress-bar__indicator' style={{ width: `${percentComplete}%` }} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,6 @@ $icon-color: $ui-white;
|
|||||||
$button-bg: $gray-1050;
|
$button-bg: $gray-1050;
|
||||||
$button-size: 48px;
|
$button-size: 48px;
|
||||||
|
|
||||||
.mirror {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonContainer {
|
.buttonContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
|
|||||||
import { navigatorConstants } from '../../../viewerConfig';
|
import { navigatorConstants } from '../../../viewerConfig';
|
||||||
import useClickOutside from '../../hooks/useClickOutside';
|
import useClickOutside from '../../hooks/useClickOutside';
|
||||||
import useFullscreen from '../../hooks/useFullscreen';
|
import useFullscreen from '../../hooks/useFullscreen';
|
||||||
import { useKeyDown } from '../../hooks/useKeyDown';
|
|
||||||
import { useViewOptionsStore } from '../../stores/viewOptions';
|
import { useViewOptionsStore } from '../../stores/viewOptions';
|
||||||
|
|
||||||
import RenameClientModal from './rename-client-modal/RenameClientModal';
|
import RenameClientModal from './rename-client-modal/RenameClientModal';
|
||||||
@@ -23,18 +22,19 @@ function NavigationMenu() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const { isFullScreen, toggleFullScreen } = useFullscreen();
|
const { isFullScreen, toggleFullScreen } = useFullscreen();
|
||||||
const { mirror, toggleMirror } = useViewOptionsStore();
|
const { toggleMirror } = useViewOptionsStore();
|
||||||
const [showButton, setShowButton] = useState(false);
|
const [showButton, setShowButton] = useState(false);
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [showMenu, setShowMenu] = useState(false);
|
const [showMenu, setShowMenu] = useState(false);
|
||||||
const menuRef = useRef<HTMLDivElement | null>(null);
|
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
useClickOutside(menuRef, () => setShowMenu(false));
|
useClickOutside(menuRef, () => setShowMenu(false));
|
||||||
|
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
|
||||||
const toggleMenu = () => setShowMenu((prev) => !prev);
|
const toggleMenu = () => setShowMenu((prev) => !prev);
|
||||||
useKeyDown(toggleMenu, ' ', { isDisabled: searchParams.get('edit') === 'true' || isOpen });
|
|
||||||
|
|
||||||
|
// show on mouse move
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let fadeOut: NodeJS.Timeout | null = null;
|
let fadeOut: NodeJS.Timeout | null = null;
|
||||||
const setShowMenuTrue = () => {
|
const setShowMenuTrue = () => {
|
||||||
@@ -63,7 +63,7 @@ function NavigationMenu() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div id='navigation-menu-portal' ref={menuRef} className={mirror ? style.mirror : ''}>
|
<div id='navigation-menu-portal' ref={menuRef}>
|
||||||
<RenameClientModal isOpen={isOpen} onClose={onClose} />
|
<RenameClientModal isOpen={isOpen} onClose={onClose} />
|
||||||
<div className={`${style.buttonContainer} ${!showButton && !showMenu ? style.hidden : ''}`}>
|
<div className={`${style.buttonContainer} ${!showButton && !showMenu ? style.hidden : ''}`}>
|
||||||
<button onClick={toggleMenu} aria-label='toggle menu' className={style.navButton}>
|
<button onClick={toggleMenu} aria-label='toggle menu' className={style.navButton}>
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
|
import SuperscriptTime from '../../../features/viewers/common/superscript-time/SuperscriptTime';
|
||||||
import { formatTime } from '../../utils/time';
|
import { formatTime } from '../../utils/time';
|
||||||
|
|
||||||
import './Schedule.scss';
|
import './Schedule.scss';
|
||||||
|
|
||||||
|
const formatOptions = {
|
||||||
|
format: 'hh:mm a',
|
||||||
|
};
|
||||||
|
|
||||||
interface ScheduleItemProps {
|
interface ScheduleItemProps {
|
||||||
selected: 'past' | 'now' | 'future';
|
selected: 'past' | 'now' | 'future';
|
||||||
timeStart: number;
|
timeStart: number;
|
||||||
@@ -16,8 +21,8 @@ interface ScheduleItemProps {
|
|||||||
export default function ScheduleItem(props: ScheduleItemProps) {
|
export default function ScheduleItem(props: ScheduleItemProps) {
|
||||||
const { selected, timeStart, timeEnd, title, presenter, backstageEvent, colour, skip } = props;
|
const { selected, timeStart, timeEnd, title, presenter, backstageEvent, colour, skip } = props;
|
||||||
|
|
||||||
const start = formatTime(timeStart, { format: 'hh:mm' });
|
const start = formatTime(timeStart, formatOptions);
|
||||||
const end = formatTime(timeEnd, { format: 'hh:mm' });
|
const end = formatTime(timeEnd, formatOptions);
|
||||||
const userColour = colour !== '' ? colour : '';
|
const userColour = colour !== '' ? colour : '';
|
||||||
const selectStyle = `entry--${selected}`;
|
const selectStyle = `entry--${selected}`;
|
||||||
|
|
||||||
@@ -25,7 +30,12 @@ export default function ScheduleItem(props: ScheduleItemProps) {
|
|||||||
<li className={`entry ${selectStyle} ${skip ? 'skip' : ''}`}>
|
<li className={`entry ${selectStyle} ${skip ? 'skip' : ''}`}>
|
||||||
<div className='entry-times'>
|
<div className='entry-times'>
|
||||||
<span className='entry-colour' style={{ backgroundColor: userColour }} />
|
<span className='entry-colour' style={{ backgroundColor: userColour }} />
|
||||||
{`${start} → ${end} ${backstageEvent ? '*' : ''}`}
|
<div style={{ display: 'flex' }}>
|
||||||
|
<SuperscriptTime time={start} />
|
||||||
|
{' → '}
|
||||||
|
<SuperscriptTime time={end} />
|
||||||
|
{backstageEvent ? '*' : ''}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='entry-title'>{title}</div>
|
<div className='entry-title'>{title}</div>
|
||||||
{presenter && <div className='entry-presenter'>{presenter}</div>}
|
{presenter && <div className='entry-presenter'>{presenter}</div>}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { Input, Select, Switch } from '@chakra-ui/react';
|
import { Input, InputGroup, InputLeftElement, Select, Switch } from '@chakra-ui/react';
|
||||||
|
|
||||||
import { isStringBoolean } from '../../utils/viewUtils';
|
import { isStringBoolean } from '../../utils/viewUtils';
|
||||||
|
|
||||||
@@ -9,16 +9,22 @@ interface EditFormInputProps {
|
|||||||
paramField: ParamField;
|
paramField: ParamField;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ParamInput({ paramField }: EditFormInputProps) {
|
export default function ParamInput(props: EditFormInputProps) {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const { id, type } = paramField;
|
const { paramField } = props;
|
||||||
|
const { id, type, defaultValue } = paramField;
|
||||||
|
|
||||||
if (type === 'option') {
|
if (type === 'option') {
|
||||||
const optionFromParams = searchParams.get(id);
|
const optionFromParams = searchParams.get(id);
|
||||||
const defaultOptionValue = optionFromParams || undefined;
|
const defaultOptionValue = optionFromParams || defaultValue;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select placeholder='Select an option' variant='ontime' name={id} defaultValue={defaultOptionValue}>
|
<Select
|
||||||
|
placeholder={defaultValue ? undefined : 'Select an option'}
|
||||||
|
variant='ontime'
|
||||||
|
name={id}
|
||||||
|
defaultValue={defaultOptionValue}
|
||||||
|
>
|
||||||
{Object.entries(paramField.values).map(([key, value]) => (
|
{Object.entries(paramField.values).map(([key, value]) => (
|
||||||
<option key={key} value={key}>
|
<option key={key} value={key}>
|
||||||
{value}
|
{value}
|
||||||
@@ -29,19 +35,38 @@ export default function ParamInput({ paramField }: EditFormInputProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'boolean') {
|
if (type === 'boolean') {
|
||||||
const defaultCheckedValue = isStringBoolean(searchParams.get(id)) ?? false;
|
const defaultCheckedValue = isStringBoolean(searchParams.get(id)) || defaultValue;
|
||||||
|
|
||||||
// checked value should be 'true', so it can be captured by the form event
|
// checked value should be 'true', so it can be captured by the form event
|
||||||
return <Switch variant='ontime' name={id} defaultChecked={defaultCheckedValue} value='true' />;
|
return <Switch variant='ontime' name={id} defaultChecked={defaultCheckedValue} value='true' />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'number') {
|
if (type === 'number') {
|
||||||
const defaultNumberValue = searchParams.get(id) ?? '';
|
const { prefix, placeholder } = paramField;
|
||||||
|
const defaultNumberValue = searchParams.get(id) ?? defaultValue;
|
||||||
|
|
||||||
return <Input type='number' step='any' variant='ontime-filled' name={id} defaultValue={defaultNumberValue} />;
|
return (
|
||||||
|
<InputGroup variant='ontime-filled'>
|
||||||
|
{prefix && <InputLeftElement pointerEvents='none'>{prefix}</InputLeftElement>}
|
||||||
|
<Input
|
||||||
|
type='number'
|
||||||
|
step='any'
|
||||||
|
variant='ontime-filled'
|
||||||
|
name={id}
|
||||||
|
defaultValue={defaultNumberValue}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultStringValue = searchParams.get(id) ?? '';
|
const defaultStringValue = searchParams.get(id) ?? defaultValue;
|
||||||
|
const { prefix, placeholder } = paramField;
|
||||||
|
|
||||||
return <Input variant='ontime-filled' name={id} defaultValue={defaultStringValue} />;
|
return (
|
||||||
|
<InputGroup variant='ontime-filled'>
|
||||||
|
{prefix && <InputLeftElement pointerEvents='none'>{prefix}</InputLeftElement>}
|
||||||
|
<Input name={id} defaultValue={defaultStringValue} placeholder={placeholder} />
|
||||||
|
</InputGroup>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,9 +22,15 @@ import style from './ViewParamsEditor.module.scss';
|
|||||||
type ViewParamsObj = { [key: string]: string | FormDataEntryValue };
|
type ViewParamsObj = { [key: string]: string | FormDataEntryValue };
|
||||||
type SavedViewParams = Record<string, ViewParamsObj>;
|
type SavedViewParams = Record<string, ViewParamsObj>;
|
||||||
|
|
||||||
const getURLSearchParamsFromObj = (paramsObj: ViewParamsObj) =>
|
const getURLSearchParamsFromObj = (paramsObj: ViewParamsObj, paramFields: ParamField[]) => {
|
||||||
Object.entries(paramsObj).reduce((newSearchParams, [id, value]) => {
|
const defaultValues = paramFields.map(({ defaultValue }) => String(defaultValue));
|
||||||
|
|
||||||
|
return Object.entries(paramsObj).reduce((newSearchParams, [id, value]) => {
|
||||||
if (typeof value === 'string' && value.length) {
|
if (typeof value === 'string' && value.length) {
|
||||||
|
if (defaultValues.includes(value)) {
|
||||||
|
return newSearchParams;
|
||||||
|
}
|
||||||
|
|
||||||
newSearchParams.set(id, value);
|
newSearchParams.set(id, value);
|
||||||
|
|
||||||
return newSearchParams;
|
return newSearchParams;
|
||||||
@@ -32,6 +38,7 @@ const getURLSearchParamsFromObj = (paramsObj: ViewParamsObj) =>
|
|||||||
|
|
||||||
return newSearchParams;
|
return newSearchParams;
|
||||||
}, new URLSearchParams());
|
}, new URLSearchParams());
|
||||||
|
};
|
||||||
|
|
||||||
interface EditFormDrawerProps {
|
interface EditFormDrawerProps {
|
||||||
paramFields: ParamField[];
|
paramFields: ParamField[];
|
||||||
@@ -51,6 +58,12 @@ export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) {
|
|||||||
}
|
}
|
||||||
}, [searchParams, onOpen]);
|
}, [searchParams, onOpen]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* disabling this for now, this feature needs more testing
|
||||||
|
* - we seem to have a bug where this is conflicting with the aliases
|
||||||
|
* - I wonder if the logic below needs to be inside an effect,
|
||||||
|
* both localStorage and searchParams should trigger a component update when they change
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const viewParamsObjFromLocalStorage = storedViewParams[pathname];
|
const viewParamsObjFromLocalStorage = storedViewParams[pathname];
|
||||||
|
|
||||||
@@ -59,36 +72,37 @@ export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) {
|
|||||||
setSearchParams(defaultSearchParams);
|
setSearchParams(defaultSearchParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
// linter is asking for `setSearchParams` in the useEffect deps
|
// linter is asking for `setSearchParams` & `storedViewParams` in the useEffect deps
|
||||||
// rule is disabled since adding `setSearchParams` results in unnecessary re-renders
|
// rule is disabled since adding `setSearchParams` & `storedViewParams` results in unnecessary re-renders
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [storedViewParams, pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
const onEditDrawerClose = () => {
|
*/
|
||||||
|
|
||||||
|
const onCloseWithoutSaving = () => {
|
||||||
onClose();
|
onClose();
|
||||||
|
|
||||||
searchParams.delete('edit');
|
searchParams.delete('edit');
|
||||||
setSearchParams(searchParams);
|
setSearchParams(searchParams);
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearParams = () => {
|
const resetParams = () => {
|
||||||
setStoredViewParams({ ...storedViewParams, [pathname]: {} });
|
setStoredViewParams({ ...storedViewParams, [pathname]: {} });
|
||||||
setSearchParams();
|
setSearchParams();
|
||||||
onClose();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onParamsFormSubmit = (formEvent: FormEvent<HTMLFormElement>) => {
|
const onParamsFormSubmit = (formEvent: FormEvent<HTMLFormElement>) => {
|
||||||
formEvent.preventDefault();
|
formEvent.preventDefault();
|
||||||
|
|
||||||
const newParamsObject = Object.fromEntries(new FormData(formEvent.currentTarget));
|
const newParamsObject = Object.fromEntries(new FormData(formEvent.currentTarget));
|
||||||
const newSearchParams = getURLSearchParamsFromObj(newParamsObject);
|
const newSearchParams = getURLSearchParamsFromObj(newParamsObject, paramFields);
|
||||||
|
|
||||||
setStoredViewParams({ ...storedViewParams, [pathname]: newParamsObject });
|
setStoredViewParams({ ...storedViewParams, [pathname]: newParamsObject });
|
||||||
setSearchParams(newSearchParams);
|
setSearchParams(newSearchParams);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer isOpen={isOpen} placement='right' onClose={onEditDrawerClose} size='lg'>
|
<Drawer isOpen={isOpen} placement='right' onClose={onCloseWithoutSaving} size='lg'>
|
||||||
<DrawerOverlay />
|
<DrawerOverlay />
|
||||||
<DrawerContent>
|
<DrawerContent>
|
||||||
<DrawerHeader className={style.drawerHeader}>
|
<DrawerHeader className={style.drawerHeader}>
|
||||||
@@ -111,10 +125,10 @@ export default function ViewParamsEditor({ paramFields }: EditFormDrawerProps) {
|
|||||||
</DrawerBody>
|
</DrawerBody>
|
||||||
|
|
||||||
<DrawerFooter className={style.drawerFooter}>
|
<DrawerFooter className={style.drawerFooter}>
|
||||||
<Button variant='ontime-ghosted' onClick={clearParams} type='reset'>
|
<Button variant='ontime-ghosted' onClick={resetParams} type='reset'>
|
||||||
Clear
|
Reset
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant='ontime-subtle' onClick={onEditDrawerClose}>
|
<Button variant='ontime-subtle' onClick={onCloseWithoutSaving}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant='ontime-filled' form='edit-params-form' type='submit'>
|
<Button variant='ontime-filled' form='edit-params-form' type='submit'>
|
||||||
|
|||||||
@@ -1,46 +1,56 @@
|
|||||||
import { UserFields } from 'ontime-types';
|
import { UserFields } from 'ontime-types';
|
||||||
|
import { TimeFormat } from 'ontime-types/src/definitions/core/TimeFormat.type';
|
||||||
|
|
||||||
import { ParamField } from './types';
|
import { ParamField } from './types';
|
||||||
|
|
||||||
export const TIME_FORMAT_OPTION: ParamField = {
|
export const getTimeOption = (timeFormat: TimeFormat): ParamField => ({
|
||||||
id: 'format',
|
id: 'format',
|
||||||
title: '12 / 24 hour timer',
|
title: '12 / 24 hour timer',
|
||||||
description: 'Whether to show the time in 12 or 24 hour mode. Overrides the global setting from preferences',
|
description: 'Whether to show the time in 12 or 24 hour mode. Overrides the global setting from preferences',
|
||||||
type: 'option',
|
type: 'option',
|
||||||
values: { '12': '12 hour AM/PM', '24': '24 hour' },
|
values: { '12': '12 hour AM/PM', '24': '24 hour' },
|
||||||
};
|
defaultValue: timeFormat,
|
||||||
|
});
|
||||||
|
|
||||||
export const CLOCK_OPTIONS: ParamField[] = [
|
export const getClockOptions = (timeFormat: TimeFormat): ParamField[] => [
|
||||||
TIME_FORMAT_OPTION,
|
getTimeOption(timeFormat),
|
||||||
{
|
{
|
||||||
id: 'key',
|
id: 'key',
|
||||||
title: 'Key Colour',
|
title: 'Key Colour',
|
||||||
description: 'Background colour in hexadecimal',
|
description: 'Background colour in hexadecimal',
|
||||||
|
prefix: '#',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
placeholder: '00000000 (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'text',
|
id: 'text',
|
||||||
title: 'Text Colour',
|
title: 'Text Colour',
|
||||||
description: 'Text colour in hexadecimal',
|
description: 'Text colour in hexadecimal',
|
||||||
|
prefix: '#',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
placeholder: 'fffff (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'textbg',
|
id: 'textbg',
|
||||||
title: 'Text Background',
|
title: 'Text Background',
|
||||||
description: 'Colour of text background in hexadecimal',
|
description: 'Colour of text background in hexadecimal',
|
||||||
|
prefix: '#',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
placeholder: '00000000 (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'font',
|
id: 'font',
|
||||||
title: 'Font',
|
title: 'Font',
|
||||||
description: 'Font family, will use the fonts available in the system',
|
description: 'Font family, will use the fonts available in the system',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
placeholder: 'Arial Black (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'size',
|
id: 'size',
|
||||||
title: 'Text Size',
|
title: 'Text Size',
|
||||||
description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)',
|
description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
placeholder: '1 (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'alignx',
|
id: 'alignx',
|
||||||
@@ -48,12 +58,14 @@ export const CLOCK_OPTIONS: ParamField[] = [
|
|||||||
description: 'Moves the horizontally in page to start = left | center | end = right',
|
description: 'Moves the horizontally in page to start = left | center | end = right',
|
||||||
type: 'option',
|
type: 'option',
|
||||||
values: { start: 'Start', center: 'Center', end: 'End' },
|
values: { start: 'Start', center: 'Center', end: 'End' },
|
||||||
|
defaultValue: 'center',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'offsetx',
|
id: 'offsetx',
|
||||||
title: 'Offset Horizontal',
|
title: 'Offset Horizontal',
|
||||||
description: 'Offsets the timer horizontal position by a given amount in pixels',
|
description: 'Offsets the timer horizontal position by a given amount in pixels',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
placeholder: '0 (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'aligny',
|
id: 'aligny',
|
||||||
@@ -61,47 +73,94 @@ export const CLOCK_OPTIONS: ParamField[] = [
|
|||||||
description: 'Moves the vertically in page to start = left | center | end = right',
|
description: 'Moves the vertically in page to start = left | center | end = right',
|
||||||
type: 'option',
|
type: 'option',
|
||||||
values: { start: 'Start', center: 'Center', end: 'End' },
|
values: { start: 'Start', center: 'Center', end: 'End' },
|
||||||
|
defaultValue: 'center',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'offsety',
|
id: 'offsety',
|
||||||
title: 'Offset Vertical',
|
title: 'Offset Vertical',
|
||||||
description: 'Offsets the timer vertical position by a given amount in pixels',
|
description: 'Offsets the timer vertical position by a given amount in pixels',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
placeholder: '0 (default)',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const TIMER_OPTIONS: ParamField[] = [TIME_FORMAT_OPTION];
|
export const getTimerOptions = (timeFormat: TimeFormat): ParamField[] => [
|
||||||
|
getTimeOption(timeFormat),
|
||||||
|
{
|
||||||
|
id: 'hideClock',
|
||||||
|
title: 'Hide Time Now',
|
||||||
|
description: 'Hides the Time Now field',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hideCards',
|
||||||
|
title: 'Hide Cards',
|
||||||
|
description: 'Hides the Now and Next cards',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hideProgress',
|
||||||
|
title: 'Hide progress bar',
|
||||||
|
description: 'Hides the progress bar',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hideMessage',
|
||||||
|
title: 'Hide Presenter Message',
|
||||||
|
description: 'Prevents the screen from displaying messages from the presenter',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hideExternal',
|
||||||
|
title: 'Hide External',
|
||||||
|
description: 'Prevents the screen from displaying the external field',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export const MINIMAL_TIMER_OPTIONS: ParamField[] = [
|
export const MINIMAL_TIMER_OPTIONS: ParamField[] = [
|
||||||
{
|
{
|
||||||
id: 'key',
|
id: 'key',
|
||||||
title: 'Key Colour',
|
title: 'Key Colour',
|
||||||
description: 'Background colour in hexadecimal',
|
description: 'Background colour in hexadecimal',
|
||||||
|
prefix: '#',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
placeholder: '00000000 (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'text',
|
id: 'text',
|
||||||
title: 'Text Colour',
|
title: 'Text Colour',
|
||||||
description: 'Text colour in hexadecimal',
|
description: 'Text colour in hexadecimal',
|
||||||
|
prefix: '#',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
placeholder: 'fffff (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'textbg',
|
id: 'textbg',
|
||||||
title: 'Text Background',
|
title: 'Text Background',
|
||||||
description: 'Colour of text background in hexadecimal',
|
description: 'Colour of text background in hexadecimal',
|
||||||
|
prefix: '#',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
placeholder: '00000000 (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'font',
|
id: 'font',
|
||||||
title: 'Font',
|
title: 'Font',
|
||||||
description: 'Font family, will use the fonts available in the system',
|
description: 'Font family, will use the fonts available in the system',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
placeholder: 'Arial Black (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'size',
|
id: 'size',
|
||||||
title: 'Text Size',
|
title: 'Text Size',
|
||||||
description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)',
|
description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
placeholder: '1 (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'alignx',
|
id: 'alignx',
|
||||||
@@ -109,12 +168,14 @@ export const MINIMAL_TIMER_OPTIONS: ParamField[] = [
|
|||||||
description: 'Moves the horizontally in page to start = left | center | end = right',
|
description: 'Moves the horizontally in page to start = left | center | end = right',
|
||||||
type: 'option',
|
type: 'option',
|
||||||
values: { start: 'Start', center: 'Center', end: 'End' },
|
values: { start: 'Start', center: 'Center', end: 'End' },
|
||||||
|
defaultValue: 'center',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'offsetx',
|
id: 'offsetx',
|
||||||
title: 'Offset Horizontal',
|
title: 'Offset Horizontal',
|
||||||
description: 'Offsets the timer horizontal position by a given amount in pixels',
|
description: 'Offsets the timer horizontal position by a given amount in pixels',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
placeholder: '0 (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'aligny',
|
id: 'aligny',
|
||||||
@@ -122,145 +183,162 @@ export const MINIMAL_TIMER_OPTIONS: ParamField[] = [
|
|||||||
description: 'Moves the vertically in page to start = left | center | end = right',
|
description: 'Moves the vertically in page to start = left | center | end = right',
|
||||||
type: 'option',
|
type: 'option',
|
||||||
values: { start: 'Start', center: 'Center', end: 'End' },
|
values: { start: 'Start', center: 'Center', end: 'End' },
|
||||||
|
defaultValue: 'center',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'offsety',
|
id: 'offsety',
|
||||||
title: 'Offset Vertical',
|
title: 'Offset Vertical',
|
||||||
description: 'Offsets the timer vertical position by a given amount in pixels',
|
description: 'Offsets the timer vertical position by a given amount in pixels',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
placeholder: '0 (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'hideovertime',
|
id: 'hideovertime',
|
||||||
title: 'Hide Overtime',
|
title: 'Hide Overtime',
|
||||||
description: 'Whether to suppress overtime styles (red borders and red text)',
|
description: 'Whether to suppress overtime styles (red borders and red text)',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'hidemessages',
|
id: 'hidemessages',
|
||||||
title: 'Hide Message Overlay',
|
title: 'Hide Message Overlay',
|
||||||
description: 'Whether to hide the overlay from showing timer screen messages',
|
description: 'Whether to hide the overlay from showing timer screen messages',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'hideendmessage',
|
id: 'hideendmessage',
|
||||||
title: 'Hide End Message',
|
title: 'Hide End Message',
|
||||||
description: 'Whether to hide end message and continue showing the clock if timer is in overtime',
|
description: 'Whether to hide end message and continue showing the clock if timer is in overtime',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const LOWER_THIRDS_OPTIONS: ParamField[] = [
|
export const LOWER_THIRDS_OPTIONS: ParamField[] = [
|
||||||
{
|
|
||||||
id: 'preset',
|
|
||||||
title: 'Preset',
|
|
||||||
description: 'Selects a style preset (0-1)',
|
|
||||||
type: 'number',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'size',
|
id: 'size',
|
||||||
title: 'Size',
|
title: 'Size',
|
||||||
description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)',
|
description: 'Scales the current style (0.5 = 50% 1 = 100% 2 = 200%)',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
placeholder: '1 (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'transition',
|
id: 'transition',
|
||||||
title: 'Transition',
|
title: 'Transition',
|
||||||
description: 'Transition in time in seconds (default 5)',
|
description: 'Transition in time in seconds (default 3)',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
placeholder: '3 (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'text',
|
id: 'text',
|
||||||
title: 'Text Colour',
|
title: 'Text Colour',
|
||||||
description: 'Text colour in hexadecimal',
|
description: 'Text colour in hexadecimal',
|
||||||
|
prefix: '#',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
placeholder: 'fffffa (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'bg',
|
id: 'bg',
|
||||||
title: 'Text Background',
|
title: 'Text Background',
|
||||||
description: 'Text background colour in hexadecimal',
|
description: 'Text background colour in hexadecimal',
|
||||||
|
prefix: '#',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
placeholder: '00000033 (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'key',
|
id: 'key',
|
||||||
title: 'Key Colour',
|
title: 'Key Colour',
|
||||||
description: 'Screen background colour in hexadecimal',
|
description: 'Screen background colour in hexadecimal',
|
||||||
|
prefix: '#',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
placeholder: '00000033 (default)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'fadeout',
|
id: 'fadeout',
|
||||||
title: 'Fadeout',
|
title: 'Fadeout',
|
||||||
description: 'Time (in seconds) the lower third displays before fading out',
|
description: 'Time (in seconds) the lower third displays before fading out',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
placeholder: '3 (default)',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const BACKSTAGE_OPTIONS: ParamField[] = [
|
export const getBackstageOptions = (timeFormat: TimeFormat): ParamField[] => [
|
||||||
TIME_FORMAT_OPTION,
|
getTimeOption(timeFormat),
|
||||||
{
|
{
|
||||||
id: 'hidePast',
|
id: 'hidePast',
|
||||||
title: 'Hide past events',
|
title: 'Hide past events',
|
||||||
description: 'Scheduler will only show upcoming events',
|
description: 'Scheduler will only show upcoming events',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'stopCycle',
|
id: 'stopCycle',
|
||||||
title: 'Stop cycling through event pages',
|
title: 'Stop cycling through event pages',
|
||||||
description: 'Schedule will not auto-cycle through events',
|
description: 'Schedule will not auto-cycle through events',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'eventsPerPage',
|
id: 'eventsPerPage',
|
||||||
title: 'Events per page',
|
title: 'Events per page',
|
||||||
description: 'Sets the number of events on the page, can cause overlow',
|
description: 'Sets the number of events on the page, can cause overlow',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
placeholder: '7 (default)',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const PUBLIC_OPTIONS: ParamField[] = [
|
export const getPublicOptions = (timeFormat: TimeFormat): ParamField[] => [
|
||||||
TIME_FORMAT_OPTION,
|
getTimeOption(timeFormat),
|
||||||
{
|
{
|
||||||
id: 'hidePast',
|
id: 'hidePast',
|
||||||
title: 'Hide past events',
|
title: 'Hide past events',
|
||||||
description: 'Scheduler will only show upcoming events',
|
description: 'Scheduler will only show upcoming events',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'stopCycle',
|
id: 'stopCycle',
|
||||||
title: 'Stop cycling through event pages',
|
title: 'Stop cycling through event pages',
|
||||||
description: 'Schedule will not auto-cycle through events',
|
description: 'Schedule will not auto-cycle through events',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'eventsPerPage',
|
id: 'eventsPerPage',
|
||||||
title: 'Events per page',
|
title: 'Events per page',
|
||||||
description: 'Sets the number of events on the page, can cause overlow',
|
description: 'Sets the number of events on the page, can cause overlow',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
placeholder: '7 (default)',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
export const STUDIO_CLOCK_OPTIONS: ParamField[] = [
|
export const getStudioClockOptions = (timeFormat: TimeFormat): ParamField[] => [
|
||||||
TIME_FORMAT_OPTION,
|
getTimeOption(timeFormat),
|
||||||
{
|
{
|
||||||
id: 'seconds',
|
id: 'seconds',
|
||||||
title: 'Show Seconds',
|
title: 'Show Seconds',
|
||||||
description: 'Shows seconds in clock',
|
description: 'Shows seconds in clock',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const getOperatorOptions = (userFields: UserFields): ParamField[] => {
|
export const getOperatorOptions = (userFields: UserFields, timeFormat: TimeFormat): ParamField[] => {
|
||||||
return [
|
return [
|
||||||
TIME_FORMAT_OPTION,
|
getTimeOption(timeFormat),
|
||||||
{
|
{
|
||||||
id: 'showseconds',
|
id: 'showseconds',
|
||||||
title: 'Show seconds',
|
title: 'Show seconds',
|
||||||
description: 'Schedule shows hh:mm:ss',
|
description: 'Schedule shows hh:mm:ss',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'hidepast',
|
id: 'hidepast',
|
||||||
title: 'Hide Past Events',
|
title: 'Hide Past Events',
|
||||||
description: 'Whether to events that have passed',
|
description: 'Whether to events that have passed',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'main',
|
id: 'main',
|
||||||
|
|||||||
@@ -4,9 +4,13 @@ type BaseField = {
|
|||||||
description: string;
|
description: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type OptionsField = { type: 'option'; values: Record<string, string> };
|
type OptionsField = {
|
||||||
type StringField = { type: 'string' };
|
type: 'option';
|
||||||
type BooleanField = { type: 'boolean' };
|
values: Record<string, string>;
|
||||||
type NumberField = { type: 'number' };
|
defaultValue?: string;
|
||||||
|
};
|
||||||
|
type StringField = { type: 'string'; defaultValue?: string; prefix?: string; placeholder?: string };
|
||||||
|
type NumberField = { type: 'number'; defaultValue?: number; prefix?: string; placeholder?: string };
|
||||||
|
type BooleanField = { type: 'boolean'; defaultValue: boolean };
|
||||||
|
|
||||||
export type ParamField = BaseField & (StringField | BooleanField | NumberField | OptionsField);
|
export type ParamField = BaseField & (StringField | BooleanField | NumberField | OptionsField);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const AppContextProvider = ({ children }: PropsWithChildren) => {
|
|||||||
const [operatorAuth, setOperatorAuth] = useState(true);
|
const [operatorAuth, setOperatorAuth] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === 'loading') return;
|
if (status === 'pending') return;
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
const previousEditor = sessionStorage.getItem(storageKeys.editor);
|
const previousEditor = sessionStorage.getItem(storageKeys.editor);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
|
import { HttpSettings } from 'ontime-types';
|
||||||
|
|
||||||
|
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||||
|
import { HTTP_SETTINGS } from '../api/apiConstants';
|
||||||
|
import { logAxiosError } from '../api/apiUtils';
|
||||||
|
import { getHTTP, postHTTP } from '../api/ontimeApi';
|
||||||
|
import { httpPlaceholder } from '../models/Http';
|
||||||
|
import { ontimeQueryClient } from '../queryClient';
|
||||||
|
|
||||||
|
export function useHttpSettings() {
|
||||||
|
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||||
|
queryKey: HTTP_SETTINGS,
|
||||||
|
queryFn: getHTTP,
|
||||||
|
placeholderData: httpPlaceholder,
|
||||||
|
retry: 5,
|
||||||
|
retryDelay: (attempt: number) => attempt * 2500,
|
||||||
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
|
networkMode: 'always',
|
||||||
|
});
|
||||||
|
|
||||||
|
// we need to jump through some hoops because of the type op port
|
||||||
|
return { data: data! as unknown as HttpSettings, status, isFetching, isError, refetch };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePostHttpSettings() {
|
||||||
|
const { isPending, mutateAsync } = useMutation({
|
||||||
|
mutationFn: postHTTP,
|
||||||
|
onError: (error) => logAxiosError('Error saving HTTP settings', error),
|
||||||
|
onSettled: () => ontimeQueryClient.invalidateQueries({ queryKey: HTTP_SETTINGS }),
|
||||||
|
});
|
||||||
|
return { isPending, mutateAsync };
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-nocheck -- working on it
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
import { OSCSettings } from 'ontime-types';
|
import { OSCSettings } from 'ontime-types';
|
||||||
|
|
||||||
@@ -24,20 +26,20 @@ export default function useOscSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useOscSettingsMutation() {
|
export function useOscSettingsMutation() {
|
||||||
const { isLoading, mutateAsync } = useMutation({
|
const { isPending, mutateAsync } = useMutation({
|
||||||
mutationFn: postOSC,
|
mutationFn: postOSC,
|
||||||
onError: (error) => logAxiosError('Error saving OSC settings', error),
|
onError: (error) => logAxiosError('Error saving OSC settings', error),
|
||||||
onSuccess: (res) => ontimeQueryClient.setQueryData(OSC_SETTINGS, res.data),
|
onSuccess: (res) => ontimeQueryClient.setQueryData(OSC_SETTINGS, res.data),
|
||||||
onSettled: () => ontimeQueryClient.invalidateQueries({ queryKey: OSC_SETTINGS }),
|
onSettled: () => ontimeQueryClient.invalidateQueries({ queryKey: OSC_SETTINGS }),
|
||||||
});
|
});
|
||||||
return { isLoading, mutateAsync };
|
return { isPending, mutateAsync };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePostOscSubscriptions() {
|
export function usePostOscSubscriptions() {
|
||||||
const { isLoading, mutateAsync } = useMutation({
|
const { isPending, mutateAsync } = useMutation({
|
||||||
mutationFn: postOscSubscriptions,
|
mutationFn: postOscSubscriptions,
|
||||||
onError: (error) => logAxiosError('Error saving OSC settings', error),
|
onError: (error) => logAxiosError('Error saving OSC settings', error),
|
||||||
onSettled: () => ontimeQueryClient.invalidateQueries({ queryKey: OSC_SETTINGS }),
|
onSettled: () => ontimeQueryClient.invalidateQueries({ queryKey: OSC_SETTINGS }),
|
||||||
});
|
});
|
||||||
return { isLoading, mutateAsync };
|
return { isPending, mutateAsync };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,29 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { GetRundownCached } from 'ontime-types';
|
||||||
|
|
||||||
import { queryRefetchInterval } from '../../ontimeConfig';
|
import { queryRefetchInterval } from '../../ontimeConfig';
|
||||||
import { RUNDOWN_TABLE } from '../api/apiConstants';
|
import { RUNDOWN } from '../api/apiConstants';
|
||||||
import { fetchRundown } from '../api/eventsApi';
|
import { fetchCachedRundown } from '../api/eventsApi';
|
||||||
|
|
||||||
|
const cachedRundownPlaceholder = { rundown: [], revision: -1 };
|
||||||
|
|
||||||
|
// TODO: can we leverage structural sharing to see if data has changed?
|
||||||
export default function useRundown() {
|
export default function useRundown() {
|
||||||
const { data, status, isError, refetch } = useQuery({
|
const { data, status, isError, refetch, isFetching } = useQuery<GetRundownCached>({
|
||||||
queryKey: RUNDOWN_TABLE,
|
queryKey: RUNDOWN,
|
||||||
queryFn: fetchRundown,
|
queryFn: fetchCachedRundown,
|
||||||
placeholderData: [],
|
placeholderData: cachedRundownPlaceholder,
|
||||||
retry: 5,
|
retry: 5,
|
||||||
retryDelay: (attempt) => attempt * 2500,
|
retryDelay: (attempt) => attempt * 2500,
|
||||||
refetchInterval: queryRefetchInterval,
|
refetchInterval: queryRefetchInterval,
|
||||||
networkMode: 'always',
|
networkMode: 'always',
|
||||||
|
// structuralSharing: (oldData: GetRundownCached | undefined, newData: GetRundownCached) => {
|
||||||
|
// if (oldData === undefined) {
|
||||||
|
// return cachedRundownPlaceholder;
|
||||||
|
// }
|
||||||
|
// const hasDataChanged = oldData?.revision === newData.revision;
|
||||||
|
// return hasDataChanged ? oldData : newData;
|
||||||
|
// },
|
||||||
});
|
});
|
||||||
|
return { data: data?.rundown ?? [], status, isError, refetch, isFetching };
|
||||||
return { data, status, isError, refetch };
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { isOntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
|
import { GetRundownCached, isOntimeEvent, OntimeRundownEntry } from 'ontime-types';
|
||||||
import { getCueCandidate, swapOntimeEvents } from 'ontime-utils';
|
import { getCueCandidate, swapOntimeEvents } from 'ontime-utils';
|
||||||
|
|
||||||
import { RUNDOWN_TABLE, RUNDOWN_TABLE_KEY } from '../api/apiConstants';
|
import { RUNDOWN } from '../api/apiConstants';
|
||||||
import { logAxiosError } from '../api/apiUtils';
|
import { logAxiosError } from '../api/apiUtils';
|
||||||
import {
|
import {
|
||||||
ReorderEntry,
|
ReorderEntry,
|
||||||
@@ -36,7 +36,7 @@ export const useEventAction = () => {
|
|||||||
// Fetch anyway, just to be sure
|
// Fetch anyway, just to be sure
|
||||||
mutationFn: requestPostEvent,
|
mutationFn: requestPostEvent,
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
queryClient.invalidateQueries(RUNDOWN_TABLE);
|
queryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||||
},
|
},
|
||||||
networkMode: 'always',
|
networkMode: 'always',
|
||||||
});
|
});
|
||||||
@@ -67,8 +67,10 @@ export const useEventAction = () => {
|
|||||||
after: options?.after,
|
after: options?.after,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rundown = queryClient.getQueryData<GetRundownCached>(RUNDOWN)?.rundown ?? [];
|
||||||
|
|
||||||
if (newEvent?.cue === undefined) {
|
if (newEvent?.cue === undefined) {
|
||||||
newEvent.cue = getCueCandidate(queryClient.getQueryData(RUNDOWN_TABLE) || [], options?.after);
|
newEvent.cue = getCueCandidate(rundown, options?.after);
|
||||||
}
|
}
|
||||||
|
|
||||||
// hard coding duration value to be as expected for now
|
// hard coding duration value to be as expected for now
|
||||||
@@ -78,7 +80,6 @@ export const useEventAction = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (applicationOptions.startTimeIsLastEnd && applicationOptions?.lastEventId) {
|
if (applicationOptions.startTimeIsLastEnd && applicationOptions?.lastEventId) {
|
||||||
const rundown = queryClient.getQueryData(RUNDOWN_TABLE) as OntimeRundown;
|
|
||||||
const previousEvent = rundown.find((event) => event.id === applicationOptions.lastEventId);
|
const previousEvent = rundown.find((event) => event.id === applicationOptions.lastEventId);
|
||||||
if (previousEvent !== undefined && previousEvent.type === 'event') {
|
if (previousEvent !== undefined && previousEvent.type === 'event') {
|
||||||
newEvent.timeStart = previousEvent.timeEnd;
|
newEvent.timeStart = previousEvent.timeEnd;
|
||||||
@@ -115,25 +116,35 @@ export const useEventAction = () => {
|
|||||||
// we optimistically update here
|
// we optimistically update here
|
||||||
onMutate: async (newEvent) => {
|
onMutate: async (newEvent) => {
|
||||||
// cancel ongoing queries
|
// cancel ongoing queries
|
||||||
await queryClient.cancelQueries([RUNDOWN_TABLE_KEY, newEvent.id]);
|
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||||
|
|
||||||
// Snapshot the previous value
|
// Snapshot the previous value
|
||||||
const previousEvent = queryClient.getQueryData([RUNDOWN_TABLE_KEY, newEvent.id]);
|
|
||||||
|
|
||||||
// optimistically update object
|
const previousData = queryClient.getQueryData<GetRundownCached>(RUNDOWN);
|
||||||
queryClient.setQueryData([RUNDOWN_TABLE_KEY, newEvent.id], newEvent);
|
|
||||||
|
if (previousData) {
|
||||||
|
// optimistically update object
|
||||||
|
const optimisticRundown = [...previousData.rundown];
|
||||||
|
const index = optimisticRundown.findIndex((event) => event.id === newEvent.id);
|
||||||
|
if (index > -1) {
|
||||||
|
// @ts-expect-error -- we expect the event types to match
|
||||||
|
optimisticRundown[index] = { ...optimisticRundown[index], ...newEvent };
|
||||||
|
|
||||||
|
queryClient.setQueryData(RUNDOWN, { rundown: optimisticRundown, revision: -1 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Return a context with the previous and new events
|
// Return a context with the previous and new events
|
||||||
return { previousEvent, newEvent };
|
return { previousData, newEvent };
|
||||||
},
|
},
|
||||||
// Mutation fails, rollback undoes optimist update
|
// Mutation fails, rollback undoes optimist update
|
||||||
onError: (_error, _newEvent, context) => {
|
onError: (_error, _newEvent, context) => {
|
||||||
queryClient.setQueryData([RUNDOWN_TABLE_KEY, context?.newEvent.id], context?.previousEvent);
|
queryClient.setQueryData(RUNDOWN, context?.previousData);
|
||||||
},
|
},
|
||||||
// Mutation finished, failed or successful
|
// Mutation finished, failed or successful
|
||||||
// Fetch anyway, just to be sure
|
// Fetch anyway, just to be sure
|
||||||
onSettled: async () => {
|
onSettled: async () => {
|
||||||
await queryClient.invalidateQueries([RUNDOWN_TABLE_KEY]);
|
await queryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||||
},
|
},
|
||||||
networkMode: 'always',
|
networkMode: 'always',
|
||||||
});
|
});
|
||||||
@@ -161,28 +172,37 @@ export const useEventAction = () => {
|
|||||||
// we optimistically update here
|
// we optimistically update here
|
||||||
onMutate: async (eventId) => {
|
onMutate: async (eventId) => {
|
||||||
// cancel ongoing queries
|
// cancel ongoing queries
|
||||||
await queryClient.cancelQueries([RUNDOWN_TABLE_KEY, eventId]);
|
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||||
|
|
||||||
// Snapshot the previous value
|
// Snapshot the previous value
|
||||||
const previousEvents = queryClient.getQueryData(RUNDOWN_TABLE);
|
const previousData = queryClient.getQueryData<GetRundownCached>(RUNDOWN);
|
||||||
|
|
||||||
const filtered = [...(previousEvents as OntimeRundown)].filter((e) => e.id !== eventId);
|
if (previousData) {
|
||||||
|
// optimistically update object
|
||||||
|
const optimisticRundown = [...previousData.rundown];
|
||||||
|
const index = optimisticRundown.findIndex((event) => event.id === eventId);
|
||||||
|
if (index > -1) {
|
||||||
|
optimisticRundown.splice(index, 1);
|
||||||
|
|
||||||
// optimistically update object
|
queryClient.setQueryData(RUNDOWN, {
|
||||||
queryClient.setQueryData(RUNDOWN_TABLE, filtered);
|
rundown: optimisticRundown,
|
||||||
|
revision: -1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Return a context with the previous and new events
|
// Return a context with the previous and new events
|
||||||
return { previousEvents };
|
return { previousData };
|
||||||
},
|
},
|
||||||
|
|
||||||
// Mutation fails, rollback undoes optimist update
|
// Mutation fails, rollback undoes optimist update
|
||||||
onError: (_error, _eventId, context) => {
|
onError: (_error, _eventId, context) => {
|
||||||
queryClient.setQueryData(RUNDOWN_TABLE, context?.previousEvents);
|
queryClient.setQueryData(RUNDOWN, context?.previousData);
|
||||||
},
|
},
|
||||||
// Mutation finished, failed or successful
|
// Mutation finished, failed or successful
|
||||||
// Fetch anyway, just to be sure
|
// Fetch anyway, just to be sure
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
queryClient.invalidateQueries(RUNDOWN_TABLE);
|
queryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||||
},
|
},
|
||||||
networkMode: 'always',
|
networkMode: 'always',
|
||||||
});
|
});
|
||||||
@@ -210,26 +230,26 @@ export const useEventAction = () => {
|
|||||||
// we optimistically update here
|
// we optimistically update here
|
||||||
onMutate: async () => {
|
onMutate: async () => {
|
||||||
// cancel ongoing queries
|
// cancel ongoing queries
|
||||||
await queryClient.cancelQueries(RUNDOWN_TABLE, { exact: true });
|
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||||
|
|
||||||
// Snapshot the previous value
|
// Snapshot the previous value
|
||||||
const previousEvents = queryClient.getQueryData(RUNDOWN_TABLE);
|
const previousData = queryClient.getQueryData<GetRundownCached>(RUNDOWN);
|
||||||
|
|
||||||
// optimistically update object
|
// optimistically update object
|
||||||
queryClient.setQueryData(RUNDOWN_TABLE, []);
|
queryClient.setQueryData(RUNDOWN, { rundown: [], revision: -1 });
|
||||||
|
|
||||||
// Return a context with the previous and new events
|
// Return a context with the previous and new events
|
||||||
return { previousEvents };
|
return { previousData };
|
||||||
},
|
},
|
||||||
|
|
||||||
// Mutation fails, rollback undos optimist update
|
// Mutation fails, rollback undos optimist update
|
||||||
onError: (_error, _eventId, context) => {
|
onError: (_error, _eventId, context) => {
|
||||||
queryClient.setQueryData(RUNDOWN_TABLE, context?.previousEvents);
|
queryClient.setQueryData(RUNDOWN, context?.previousData);
|
||||||
},
|
},
|
||||||
// Mutation finished, failed or successful
|
// Mutation finished, failed or successful
|
||||||
// Fetch anyway, just to be sure
|
// Fetch anyway, just to be sure
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
queryClient.invalidateQueries(RUNDOWN_TABLE);
|
queryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||||
},
|
},
|
||||||
networkMode: 'always',
|
networkMode: 'always',
|
||||||
});
|
});
|
||||||
@@ -253,7 +273,7 @@ export const useEventAction = () => {
|
|||||||
mutationFn: requestApplyDelay,
|
mutationFn: requestApplyDelay,
|
||||||
// Mutation finished, failed or successful
|
// Mutation finished, failed or successful
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
queryClient.invalidateQueries(RUNDOWN_TABLE);
|
queryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||||
},
|
},
|
||||||
networkMode: 'always',
|
networkMode: 'always',
|
||||||
});
|
});
|
||||||
@@ -281,30 +301,32 @@ export const useEventAction = () => {
|
|||||||
// we optimistically update here
|
// we optimistically update here
|
||||||
onMutate: async (data) => {
|
onMutate: async (data) => {
|
||||||
// cancel ongoing queries
|
// cancel ongoing queries
|
||||||
await queryClient.cancelQueries(RUNDOWN_TABLE, { exact: true });
|
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||||
|
|
||||||
// Snapshot the previous value
|
// Snapshot the previous value
|
||||||
const previousEvents = queryClient.getQueryData(RUNDOWN_TABLE);
|
const previousData = queryClient.getQueryData<GetRundownCached>(RUNDOWN);
|
||||||
|
|
||||||
const e = [...(previousEvents as OntimeRundown)];
|
if (previousData) {
|
||||||
const [reorderedItem] = e.splice(data.from, 1);
|
// optimistically update object
|
||||||
e.splice(data.to, 0, reorderedItem);
|
const optimisticRundown = [...previousData.rundown];
|
||||||
|
const [reorderedItem] = optimisticRundown.splice(data.from, 1);
|
||||||
|
optimisticRundown.splice(data.to, 0, reorderedItem);
|
||||||
|
|
||||||
// optimistically update object
|
queryClient.setQueryData(RUNDOWN, { rundown: optimisticRundown, revision: -1 });
|
||||||
queryClient.setQueryData(RUNDOWN_TABLE, e);
|
}
|
||||||
|
|
||||||
// Return a context with the previous and new events
|
// Return a context with the previous and new events
|
||||||
return { previousEvents };
|
return { previousData };
|
||||||
},
|
},
|
||||||
|
|
||||||
// Mutation fails, rollback undoes optimist update
|
// Mutation fails, rollback undoes optimist update
|
||||||
onError: (_error, _eventId, context) => {
|
onError: (_error, _eventId, context) => {
|
||||||
queryClient.setQueryData(RUNDOWN_TABLE, context?.previousEvents);
|
queryClient.setQueryData(RUNDOWN, context?.previousData);
|
||||||
},
|
},
|
||||||
// Mutation finished, failed or successful
|
// Mutation finished, failed or successful
|
||||||
// Fetch anyway, just to be sure
|
// Fetch anyway, just to be sure
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
queryClient.invalidateQueries(RUNDOWN_TABLE);
|
queryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||||
},
|
},
|
||||||
networkMode: 'always',
|
networkMode: 'always',
|
||||||
});
|
});
|
||||||
@@ -337,31 +359,32 @@ export const useEventAction = () => {
|
|||||||
// we optimistically update here
|
// we optimistically update here
|
||||||
onMutate: async ({ from, to }) => {
|
onMutate: async ({ from, to }) => {
|
||||||
// cancel ongoing queries
|
// cancel ongoing queries
|
||||||
await queryClient.cancelQueries(RUNDOWN_TABLE, { exact: true });
|
await queryClient.cancelQueries({ queryKey: RUNDOWN });
|
||||||
|
|
||||||
// Snapshot the previous value
|
// Snapshot the previous value
|
||||||
const rundown = queryClient.getQueryData(RUNDOWN_TABLE) as OntimeRundown;
|
const previousData = queryClient.getQueryData<GetRundownCached>(RUNDOWN);
|
||||||
|
if (previousData) {
|
||||||
|
// optimistically update object
|
||||||
|
const fromEventIndex = previousData.rundown.findIndex((event) => event.id === from);
|
||||||
|
const toEventIndex = previousData.rundown.findIndex((event) => event.id === to);
|
||||||
|
|
||||||
const fromEventIndex = rundown.findIndex((event) => event.id === from);
|
const optimisticRundown = swapOntimeEvents(previousData.rundown, fromEventIndex, toEventIndex);
|
||||||
const toEventIndex = rundown.findIndex((event) => event.id === to);
|
|
||||||
|
|
||||||
const previousEvents = swapOntimeEvents(rundown, fromEventIndex, toEventIndex);
|
queryClient.setQueryData(RUNDOWN, { rundown: optimisticRundown, revision: -1 });
|
||||||
|
}
|
||||||
// optimistically update object
|
|
||||||
queryClient.setQueryData(RUNDOWN_TABLE, previousEvents);
|
|
||||||
|
|
||||||
// Return a context with the previous events
|
// Return a context with the previous events
|
||||||
return { previousEvents };
|
return { previousData };
|
||||||
},
|
},
|
||||||
|
|
||||||
// Mutation fails, rollback undoes optimist update
|
// Mutation fails, rollback undoes optimist update
|
||||||
onError: (_error, _eventId, context) => {
|
onError: (_error, _eventId, context) => {
|
||||||
queryClient.setQueryData(RUNDOWN_TABLE, context?.previousEvents);
|
queryClient.setQueryData(RUNDOWN, context?.previousData);
|
||||||
},
|
},
|
||||||
// Mutation finished, failed or successful
|
// Mutation finished, failed or successful
|
||||||
// Fetch anyway, just to be sure
|
// Fetch anyway, just to be sure
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
queryClient.invalidateQueries(RUNDOWN_TABLE);
|
queryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||||
},
|
},
|
||||||
networkMode: 'always',
|
networkMode: 'always',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ interface UseFollowComponentProps {
|
|||||||
scrollRef: MutableRefObject<HTMLElement | null>;
|
scrollRef: MutableRefObject<HTMLElement | null>;
|
||||||
doFollow: boolean;
|
doFollow: boolean;
|
||||||
topOffset?: number;
|
topOffset?: number;
|
||||||
setScrollFlag?: () => void;
|
setScrollFlag?: (newValue: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function useFollowComponent(props: UseFollowComponentProps) {
|
export default function useFollowComponent(props: UseFollowComponentProps) {
|
||||||
@@ -34,14 +34,15 @@ export default function useFollowComponent(props: UseFollowComponentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (followRef.current && scrollRef.current) {
|
if (followRef.current && scrollRef.current) {
|
||||||
|
setScrollFlag?.(true);
|
||||||
// Use requestAnimationFrame to ensure the component is fully loaded
|
// Use requestAnimationFrame to ensure the component is fully loaded
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
setScrollFlag?.();
|
|
||||||
scrollToComponent(
|
scrollToComponent(
|
||||||
followRef as MutableRefObject<HTMLElement>,
|
followRef as MutableRefObject<HTMLElement>,
|
||||||
scrollRef as MutableRefObject<HTMLElement>,
|
scrollRef as MutableRefObject<HTMLElement>,
|
||||||
topOffset,
|
topOffset,
|
||||||
);
|
);
|
||||||
|
setScrollFlag?.(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-nocheck -- working on it
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
interface WebkitDocument extends Document {
|
interface WebkitDocument extends Document {
|
||||||
|
|||||||
@@ -1,53 +1,46 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useSyncExternalStore } from 'react';
|
||||||
|
|
||||||
/**
|
const STORAGE_EVENT = 'ontime-storage';
|
||||||
* @description utility hook to handle state in local storage
|
|
||||||
* @param key
|
|
||||||
* @param initialValue
|
|
||||||
*/
|
|
||||||
export const useLocalStorage = <T>(key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void] => {
|
|
||||||
const [storedValue, setStoredValue] = useState<T>(() => {
|
|
||||||
try {
|
|
||||||
const item = window.localStorage.getItem(`ontime-${key}`);
|
|
||||||
return item ? JSON.parse(item) : initialValue;
|
|
||||||
} catch (error) {
|
|
||||||
return initialValue;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
function getSnapshot(key: string): string | null {
|
||||||
const handleStorageChange = (event: StorageEvent) => {
|
try {
|
||||||
if (event.storageArea === window.localStorage && event.key === key) {
|
return window.localStorage.getItem(`ontime-${key}`);
|
||||||
try {
|
} catch {
|
||||||
const newValue = event.newValue ? JSON.parse(event.newValue) : initialValue;
|
return null;
|
||||||
setStoredValue(newValue);
|
}
|
||||||
} catch (_) {
|
}
|
||||||
/* empty */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('storage', handleStorageChange);
|
function getParsedJson<T>(localStorageValue: string | null, initialValue: T): T {
|
||||||
|
try {
|
||||||
|
return localStorageValue ? JSON.parse(localStorageValue) : initialValue;
|
||||||
|
} catch {
|
||||||
|
return initialValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
export const useLocalStorage = <T>(key: string, initialValue: T) => {
|
||||||
window.removeEventListener('storage', handleStorageChange);
|
const localStorageValue = useSyncExternalStore(subscribe, () => getSnapshot(key));
|
||||||
};
|
const parsedLocalStorageValue = getParsedJson(localStorageValue, initialValue);
|
||||||
}, [initialValue, key]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Set value to local storage
|
* @description Set value to local storage
|
||||||
* @param value
|
* @param value
|
||||||
*/
|
*/
|
||||||
const setValue = (value: T | ((val: T) => T)) => {
|
const setLocalStorageValue = (value: T | ((val: T) => T)) => {
|
||||||
try {
|
// Allow value to be a function so we have same API as useState
|
||||||
// Allow value to be a function so we have same API as useState
|
const valueToStore = value instanceof Function ? value(parsedLocalStorageValue) : value;
|
||||||
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
|
||||||
|
|
||||||
setStoredValue(valueToStore);
|
localStorage.setItem(`ontime-${key}`, JSON.stringify(valueToStore));
|
||||||
window.localStorage.setItem(`ontime-${key}`, JSON.stringify(valueToStore));
|
window.dispatchEvent(new StorageEvent(STORAGE_EVENT));
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
return [storedValue, setValue];
|
|
||||||
|
return [parsedLocalStorageValue, setLocalStorageValue] as const;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function subscribe(callback: () => void) {
|
||||||
|
window.addEventListener(STORAGE_EVENT, callback);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener(STORAGE_EVENT, callback);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { MouseEvent, SyntheticEvent, TouchEvent, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
|
type LongPressOptions = {
|
||||||
|
threshold?: number;
|
||||||
|
onStart?: (e: SyntheticEvent) => void;
|
||||||
|
onFinish?: (e: SyntheticEvent) => void;
|
||||||
|
onCancel?: (e: SyntheticEvent) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LongPressFns = {
|
||||||
|
onMouseDown: (e: MouseEvent) => void;
|
||||||
|
onMouseUp: (e: MouseEvent) => void;
|
||||||
|
onMouseLeave: (e: MouseEvent) => void;
|
||||||
|
onTouchStart: (e: TouchEvent) => void;
|
||||||
|
onTouchEnd: (e: TouchEvent) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function useLongPress(callback: () => void, options: LongPressOptions = {}): LongPressFns {
|
||||||
|
const { threshold = 400, onStart, onFinish, onCancel } = options;
|
||||||
|
const isLongPressActive = useRef(false);
|
||||||
|
const isPressed = useRef(false);
|
||||||
|
const timerId = useRef<NodeJS.Timer>();
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const start = (event: SyntheticEvent) => {
|
||||||
|
if (onStart) {
|
||||||
|
onStart(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
isPressed.current = true;
|
||||||
|
timerId.current = setTimeout(() => {
|
||||||
|
callback();
|
||||||
|
isLongPressActive.current = true;
|
||||||
|
}, threshold);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancel = (event: SyntheticEvent) => {
|
||||||
|
if (isLongPressActive.current) {
|
||||||
|
if (onFinish) {
|
||||||
|
onFinish(event);
|
||||||
|
}
|
||||||
|
} else if (isPressed.current) {
|
||||||
|
if (onCancel) {
|
||||||
|
onCancel(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isLongPressActive.current = false;
|
||||||
|
isPressed.current = false;
|
||||||
|
|
||||||
|
if (timerId.current) {
|
||||||
|
clearTimeout(timerId.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
onMouseDown: start,
|
||||||
|
onMouseUp: cancel,
|
||||||
|
onMouseLeave: cancel,
|
||||||
|
onTouchStart: start,
|
||||||
|
onTouchEnd: cancel,
|
||||||
|
};
|
||||||
|
}, [callback, threshold, onCancel, onFinish, onStart]);
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ export const useMessageControl = () => {
|
|||||||
timerMessage: state.timerMessage,
|
timerMessage: state.timerMessage,
|
||||||
publicMessage: state.publicMessage,
|
publicMessage: state.publicMessage,
|
||||||
lowerMessage: state.lowerMessage,
|
lowerMessage: state.lowerMessage,
|
||||||
|
externalMessage: state.externalMessage,
|
||||||
onAir: state.onAir,
|
onAir: state.onAir,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -40,6 +41,8 @@ export const setMessage = {
|
|||||||
publicVisible: (payload: boolean) => socketSendJson('set-public-message-visible', payload),
|
publicVisible: (payload: boolean) => socketSendJson('set-public-message-visible', payload),
|
||||||
lowerText: (payload: string) => socketSendJson('set-lower-message-text', payload),
|
lowerText: (payload: string) => socketSendJson('set-lower-message-text', payload),
|
||||||
lowerVisible: (payload: boolean) => socketSendJson('set-lower-message-visible', payload),
|
lowerVisible: (payload: boolean) => socketSendJson('set-lower-message-visible', payload),
|
||||||
|
externalText: (payload: string) => socketSendJson('set-external-message-text', payload),
|
||||||
|
externalVisible: (payload: boolean) => socketSendJson('set-external-message-visible', payload),
|
||||||
onAir: (payload: boolean) => socketSendJson('set-onAir', payload),
|
onAir: (payload: boolean) => socketSendJson('set-onAir', payload),
|
||||||
timerBlink: (payload: boolean) => socketSendJson('set-timer-blink', payload),
|
timerBlink: (payload: boolean) => socketSendJson('set-timer-blink', payload),
|
||||||
timerBlackout: (payload: boolean) => socketSendJson('set-timer-blackout', payload),
|
timerBlackout: (payload: boolean) => socketSendJson('set-timer-blackout', payload),
|
||||||
@@ -72,8 +75,8 @@ export const setPlayback = {
|
|||||||
reload: () => {
|
reload: () => {
|
||||||
socketSendJson('reload');
|
socketSendJson('reload');
|
||||||
},
|
},
|
||||||
delay: (amount: number) => {
|
addTime: (amount: number) => {
|
||||||
socketSendJson('delay', amount);
|
socketSendJson('addtime', amount);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,13 @@
|
|||||||
export const httpPlaceholder = {
|
import { HttpSettings } from 'ontime-types';
|
||||||
onLoad: {
|
|
||||||
url: '',
|
export const httpPlaceholder: HttpSettings = {
|
||||||
enabled: false,
|
enabledOut: false,
|
||||||
},
|
subscriptions: {
|
||||||
onStart: {
|
onLoad: [],
|
||||||
url: '',
|
onStart: [],
|
||||||
enabled: false,
|
onUpdate: [],
|
||||||
},
|
onPause: [],
|
||||||
onUpdate: {
|
onStop: [],
|
||||||
url: '',
|
onFinish: [],
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
onPause: {
|
|
||||||
url: '',
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
onStop: {
|
|
||||||
url: '',
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
onFinish: {
|
|
||||||
url: '',
|
|
||||||
enabled: false,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { QueryClient } from '@tanstack/react-query';
|
|||||||
export const ontimeQueryClient = new QueryClient({
|
export const ontimeQueryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
cacheTime: 1000 * 60 * 10, // 10 min
|
gcTime: 1000 * 60 * 10, // 10 min
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Playback, RuntimeStore } from 'ontime-types';
|
|||||||
import { useStore } from 'zustand';
|
import { useStore } from 'zustand';
|
||||||
import { createStore } from 'zustand/vanilla';
|
import { createStore } from 'zustand/vanilla';
|
||||||
|
|
||||||
export const runtimeStorePlaceholder = {
|
export const runtimeStorePlaceholder: RuntimeStore = {
|
||||||
timer: {
|
timer: {
|
||||||
clock: 0,
|
clock: 0,
|
||||||
current: null,
|
current: null,
|
||||||
@@ -33,6 +33,10 @@ export const runtimeStorePlaceholder = {
|
|||||||
text: '',
|
text: '',
|
||||||
visible: false,
|
visible: false,
|
||||||
},
|
},
|
||||||
|
externalMessage: {
|
||||||
|
text: '',
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
onAir: false,
|
onAir: false,
|
||||||
loaded: {
|
loaded: {
|
||||||
numEvents: 0,
|
numEvents: 0,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isIPAddress, isOnlyNumbers } from '../regex';
|
import { isIPAddress, isOnlyNumbers, startsWithHttp } from '../regex';
|
||||||
|
|
||||||
describe('simple tests for regex', () => {
|
describe('simple tests for regex', () => {
|
||||||
test('isOnlyNumbers', () => {
|
test('isOnlyNumbers', () => {
|
||||||
@@ -24,4 +24,16 @@ describe('simple tests for regex', () => {
|
|||||||
expect(isIPAddress.test(t)).toBe(false);
|
expect(isIPAddress.test(t)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('startsWithHttp', () => {
|
||||||
|
const right = ['http://test'];
|
||||||
|
const wrong = ['https://test', 'testing', '123.0.1'];
|
||||||
|
|
||||||
|
right.forEach((t) => {
|
||||||
|
expect(startsWithHttp.test(t)).toBe(true);
|
||||||
|
});
|
||||||
|
wrong.forEach((t) => {
|
||||||
|
expect(startsWithHttp.test(t)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { cx } from '../styleUtils';
|
import { cx, getAccessibleColour } from '../styleUtils';
|
||||||
|
|
||||||
import style from './styleUtils.module.scss';
|
import style from './styleUtils.module.scss';
|
||||||
|
|
||||||
@@ -13,3 +13,24 @@ describe('cx()', () => {
|
|||||||
expect(merged).toMatchSnapshot();
|
expect(merged).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getAccessibleColour()', () => {
|
||||||
|
it('handles named colours', () => {
|
||||||
|
const colour = 'red';
|
||||||
|
const { backgroundColor, color } = getAccessibleColour(colour);
|
||||||
|
expect(backgroundColor).toBe('#FF0000FF');
|
||||||
|
expect(color).toBe('#fffffa');
|
||||||
|
});
|
||||||
|
it('handles hex colours', () => {
|
||||||
|
const colour = '#0F0';
|
||||||
|
const { backgroundColor, color } = getAccessibleColour(colour);
|
||||||
|
expect(backgroundColor).toBe('#00FF00FF');
|
||||||
|
expect(color).toBe('black');
|
||||||
|
});
|
||||||
|
it('handles transparens', () => {
|
||||||
|
const colour = '#0F08';
|
||||||
|
const { backgroundColor, color } = getAccessibleColour(colour);
|
||||||
|
expect(backgroundColor).toBe('#0C940CFF');
|
||||||
|
expect(color).toBe('#fffffa');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+10
@@ -26,4 +26,14 @@ describe('formatTime()', () => {
|
|||||||
const time = formatTime(ms);
|
const time = formatTime(ms);
|
||||||
expect(time).toStrictEqual('...');
|
expect(time).toStrictEqual('...');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows 12h format without times', () => {
|
||||||
|
const ms = 13 * 60 * 60 * 1000;
|
||||||
|
const options = {
|
||||||
|
showSeconds: false,
|
||||||
|
format: 'hh:mm a',
|
||||||
|
};
|
||||||
|
const time = formatTime(ms, options, () => '12');
|
||||||
|
expect(time).toStrictEqual('01:00 PM');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export const isOnlyNumbers = /^\d+$/;
|
export const isOnlyNumbers = /^\d+$/;
|
||||||
export const isIPAddress = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/;
|
export const isIPAddress = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/;
|
||||||
|
export const startsWithHttp = /^http:\/\//;
|
||||||
|
|||||||
@@ -13,13 +13,15 @@ type ColourCombination = {
|
|||||||
export const getAccessibleColour = (bgColour?: string): ColourCombination => {
|
export const getAccessibleColour = (bgColour?: string): ColourCombination => {
|
||||||
if (bgColour) {
|
if (bgColour) {
|
||||||
try {
|
try {
|
||||||
const textColor = Color(bgColour).isLight() ? 'black' : '#fffffa';
|
const originalColour = Color(bgColour);
|
||||||
return { backgroundColor: bgColour, color: textColor };
|
const backgroundColorMix = originalColour.alpha(1).mix(Color('#1a1a1a'), 1 - originalColour.alpha());
|
||||||
|
const textColor = backgroundColorMix.isLight() ? 'black' : '#fffffa';
|
||||||
|
return { backgroundColor: backgroundColorMix.hexa(), color: textColor };
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
/* we do not handle errors here */
|
/* we do not handle errors here */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { backgroundColor: '#000', color: '#fffffa' };
|
return { backgroundColor: '#1a1a1a', color: '#fffffa' };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Input } from '@chakra-ui/react';
|
import { IconButton, Input } from '@chakra-ui/react';
|
||||||
import { IoEye } from '@react-icons/all-files/io5/IoEye';
|
import { IoEye } from '@react-icons/all-files/io5/IoEye';
|
||||||
import { IoEyeOffOutline } from '@react-icons/all-files/io5/IoEyeOffOutline';
|
import { IoEyeOffOutline } from '@react-icons/all-files/io5/IoEyeOffOutline';
|
||||||
|
|
||||||
@@ -13,13 +13,14 @@ interface InputRowProps {
|
|||||||
placeholder: string;
|
placeholder: string;
|
||||||
text: string;
|
text: string;
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
|
readonly?: boolean;
|
||||||
actionHandler: (action: string, payload: object) => void;
|
actionHandler: (action: string, payload: object) => void;
|
||||||
changeHandler: (newValue: string) => void;
|
changeHandler: (newValue: string) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function InputRow(props: InputRowProps) {
|
export default function InputRow(props: InputRowProps) {
|
||||||
const { label, placeholder, text, visible, actionHandler, changeHandler, className } = props;
|
const { label, placeholder, text, visible, actionHandler, changeHandler, className, readonly } = props;
|
||||||
|
|
||||||
const handleInputChange = (newValue: string) => {
|
const handleInputChange = (newValue: string) => {
|
||||||
changeHandler(newValue);
|
changeHandler(newValue);
|
||||||
@@ -33,19 +34,31 @@ export default function InputRow(props: InputRowProps) {
|
|||||||
<Input
|
<Input
|
||||||
size='sm'
|
size='sm'
|
||||||
variant='ontime-filled'
|
variant='ontime-filled'
|
||||||
|
readOnly={readonly}
|
||||||
|
disabled={readonly}
|
||||||
value={text}
|
value={text}
|
||||||
onChange={(event) => handleInputChange(event.target.value)}
|
onChange={(event) => handleInputChange(event.target.value)}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
/>
|
/>
|
||||||
<TooltipActionBtn
|
{readonly ? (
|
||||||
clickHandler={() => actionHandler('update', { field: 'isPublic', value: !visible })}
|
<IconButton
|
||||||
tooltip={visible ? 'Make invisible' : 'Make visible'}
|
size='sm'
|
||||||
aria-label={`Toggle ${label}`}
|
isDisabled
|
||||||
openDelay={tooltipDelayMid}
|
icon={visible ? <IoEye size='18px' /> : <IoEyeOffOutline size='18px' />}
|
||||||
icon={visible ? <IoEye size='18px' /> : <IoEyeOffOutline size='18px' />}
|
aria-label={`Toggle ${label}`}
|
||||||
variant={visible ? 'ontime-filled' : 'ontime-subtle'}
|
variant={visible ? 'ontime-filled' : 'ontime-subtle'}
|
||||||
size='sm'
|
/>
|
||||||
/>
|
) : (
|
||||||
|
<TooltipActionBtn
|
||||||
|
clickHandler={() => actionHandler('update', { field: 'isPublic', value: !visible })}
|
||||||
|
tooltip={visible ? 'Make invisible' : 'Make visible'}
|
||||||
|
aria-label={`Toggle ${label}`}
|
||||||
|
openDelay={tooltipDelayMid}
|
||||||
|
icon={visible ? <IoEye size='18px' /> : <IoEyeOffOutline size='18px' />}
|
||||||
|
variant={visible ? 'ontime-filled' : 'ontime-subtle'}
|
||||||
|
size='sm'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -27,7 +27,3 @@
|
|||||||
color: $action-text-color;
|
color: $action-text-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.padTop {
|
|
||||||
margin-top: $section-spacing;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -34,34 +34,48 @@ export default function MessageControl() {
|
|||||||
actionHandler={() => setMessage.lowerVisible(!data.lowerMessage.visible)}
|
actionHandler={() => setMessage.lowerVisible(!data.lowerMessage.visible)}
|
||||||
/>
|
/>
|
||||||
<InputRow
|
<InputRow
|
||||||
label='Timer message'
|
label='Timer'
|
||||||
placeholder='Shown in stage timer'
|
placeholder='Message shown in stage timer'
|
||||||
text={data.timerMessage.text || ''}
|
text={data.timerMessage.text || ''}
|
||||||
visible={data.timerMessage.visible || false}
|
visible={data.timerMessage.visible || false}
|
||||||
changeHandler={(newValue) => setMessage.presenterText(newValue)}
|
changeHandler={(newValue) => setMessage.presenterText(newValue)}
|
||||||
actionHandler={() => setMessage.presenterVisible(!data.timerMessage.visible)}
|
actionHandler={() => setMessage.presenterVisible(!data.timerMessage.visible)}
|
||||||
/>
|
/>
|
||||||
<div className={style.buttonSection}>
|
<div className={style.buttonSection}>
|
||||||
<label className={style.label}>Timer message blink</label>
|
|
||||||
<label className={style.label}>Blackout timer screens</label>
|
|
||||||
<Button
|
<Button
|
||||||
|
size='sm'
|
||||||
className={`${data.timerMessage.timerBlink ? style.blink : ''}`}
|
className={`${data.timerMessage.timerBlink ? style.blink : ''}`}
|
||||||
variant={data.timerMessage.timerBlink ? 'ontime-filled' : 'ontime-subtle'}
|
variant={data.timerMessage.timerBlink ? 'ontime-filled' : 'ontime-subtle'}
|
||||||
leftIcon={data.timerMessage.timerBlink ? <IoSunny size='24px' /> : <IoSunnyOutline size='24px' />}
|
leftIcon={data.timerMessage.timerBlink ? <IoSunny size='1rem' /> : <IoSunnyOutline size='1rem' />}
|
||||||
onClick={() => setMessage.timerBlink(!data.timerMessage.timerBlink)}
|
onClick={() => setMessage.timerBlink(!data.timerMessage.timerBlink)}
|
||||||
data-testid='toggle timer blink'
|
data-testid='toggle timer blink'
|
||||||
/>
|
>
|
||||||
|
Blink message
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
size='sm'
|
||||||
className={style.blackoutButton}
|
className={style.blackoutButton}
|
||||||
variant={data.timerMessage.timerBlackout ? 'ontime-filled' : 'ontime-subtle'}
|
variant={data.timerMessage.timerBlackout ? 'ontime-filled' : 'ontime-subtle'}
|
||||||
leftIcon={data.timerMessage.timerBlackout ? <IoEye size='24px' /> : <IoEyeOffOutline size='24px' />}
|
leftIcon={data.timerMessage.timerBlackout ? <IoEye size='1rem' /> : <IoEyeOffOutline size='1rem' />}
|
||||||
onClick={() => setMessage.timerBlackout(!data.timerMessage.timerBlackout)}
|
onClick={() => setMessage.timerBlackout(!data.timerMessage.timerBlackout)}
|
||||||
data-testid='toggle timer blackout'
|
data-testid='toggle timer blackout'
|
||||||
/>
|
>
|
||||||
|
Blackout screen
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className={`${style.onAirSection} ${style.padTop}`}>
|
<InputRow
|
||||||
|
label='External Message'
|
||||||
|
placeholder='-'
|
||||||
|
readonly
|
||||||
|
text={data.externalMessage.text || ''}
|
||||||
|
visible={data.externalMessage.visible || false}
|
||||||
|
changeHandler={() => undefined}
|
||||||
|
actionHandler={() => undefined}
|
||||||
|
/>
|
||||||
|
<div className={style.onAirSection}>
|
||||||
<label className={style.label}>Toggle On Air state</label>
|
<label className={style.label}>Toggle On Air state</label>
|
||||||
<Button
|
<Button
|
||||||
|
size='sm'
|
||||||
variant={data.onAir ? 'ontime-filled' : 'ontime-subtle'}
|
variant={data.onAir ? 'ontime-filled' : 'ontime-subtle'}
|
||||||
leftIcon={data.onAir ? <IoMicSharp size='24px' /> : <IoMicOffOutline size='24px' />}
|
leftIcon={data.onAir ? <IoMicSharp size='24px' /> : <IoMicOffOutline size='24px' />}
|
||||||
onClick={() => setMessage.onAir(!data.onAir)}
|
onClick={() => setMessage.onAir(!data.onAir)}
|
||||||
|
|||||||
@@ -86,22 +86,22 @@ export default function PlaybackTimer(props: PlaybackTimerProps) {
|
|||||||
)}
|
)}
|
||||||
<div className={style.btn}>
|
<div className={style.btn}>
|
||||||
<Tooltip label='Remove 1 minute' openDelay={tooltipDelayMid} shouldWrapChildren={disableButtons}>
|
<Tooltip label='Remove 1 minute' openDelay={tooltipDelayMid} shouldWrapChildren={disableButtons}>
|
||||||
<TapButton onClick={() => setPlayback.delay(-1)} disabled={disableButtons} aspect='square'>
|
<TapButton onClick={() => setPlayback.addTime(-60)} disabled={disableButtons} aspect='square'>
|
||||||
-1
|
-1
|
||||||
</TapButton>
|
</TapButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip label='Add 1 minute' openDelay={tooltipDelayMid} shouldWrapChildren={disableButtons}>
|
<Tooltip label='Add 1 minute' openDelay={tooltipDelayMid} shouldWrapChildren={disableButtons}>
|
||||||
<TapButton onClick={() => setPlayback.delay(1)} disabled={disableButtons} aspect='square'>
|
<TapButton onClick={() => setPlayback.addTime(60)} disabled={disableButtons} aspect='square'>
|
||||||
+1
|
+1
|
||||||
</TapButton>
|
</TapButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip label='Remove 5 minutes' openDelay={tooltipDelayMid} shouldWrapChildren={disableButtons}>
|
<Tooltip label='Remove 5 minutes' openDelay={tooltipDelayMid} shouldWrapChildren={disableButtons}>
|
||||||
<TapButton onClick={() => setPlayback.delay(-5)} disabled={disableButtons} aspect='square'>
|
<TapButton onClick={() => setPlayback.addTime(-5 * 60)} disabled={disableButtons} aspect='square'>
|
||||||
-5
|
-5
|
||||||
</TapButton>
|
</TapButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip label='Add 5 minutes' openDelay={tooltipDelayMid} shouldWrapChildren={disableButtons}>
|
<Tooltip label='Add 5 minutes' openDelay={tooltipDelayMid} shouldWrapChildren={disableButtons}>
|
||||||
<TapButton onClick={() => setPlayback.delay(+5)} disabled={disableButtons} aspect='square'>
|
<TapButton onClick={() => setPlayback.addTime(+5 * 60)} disabled={disableButtons} aspect='square'>
|
||||||
+5
|
+5
|
||||||
</TapButton>
|
</TapButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { isOntimeBlock, isOntimeDelay, isOntimeEvent, OntimeRundown, OntimeRundo
|
|||||||
|
|
||||||
import useFollowComponent from '../../common/hooks/useFollowComponent';
|
import useFollowComponent from '../../common/hooks/useFollowComponent';
|
||||||
import { useLocalStorage } from '../../common/hooks/useLocalStorage';
|
import { useLocalStorage } from '../../common/hooks/useLocalStorage';
|
||||||
|
import { getAccessibleColour } from '../../common/utils/styleUtils';
|
||||||
|
|
||||||
import BlockRow from './cuesheet-table-elements/BlockRow';
|
import BlockRow from './cuesheet-table-elements/BlockRow';
|
||||||
import CuesheetHeader from './cuesheet-table-elements/CuesheetHeader';
|
import CuesheetHeader from './cuesheet-table-elements/CuesheetHeader';
|
||||||
@@ -24,10 +25,7 @@ interface CuesheetProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Cuesheet({ data, columns, handleUpdate, selectedId }: CuesheetProps) {
|
export default function Cuesheet({ data, columns, handleUpdate, selectedId }: CuesheetProps) {
|
||||||
const followSelected = useCuesheetSettings((state) => state.followSelected);
|
const { followSelected, showSettings, showDelayBlock, showPrevious } = useCuesheetSettings();
|
||||||
const showSettings = useCuesheetSettings((state) => state.showSettings);
|
|
||||||
const showDelayBlock = useCuesheetSettings((state) => state.showDelayBlock);
|
|
||||||
const showPrevious = useCuesheetSettings((state) => state.showPrevious);
|
|
||||||
|
|
||||||
const [columnVisibility, setColumnVisibility] = useLocalStorage('table-hidden', {});
|
const [columnVisibility, setColumnVisibility] = useLocalStorage('table-hidden', {});
|
||||||
const [columnOrder, saveColumnOrder] = useLocalStorage<string[]>('table-order', initialColumnOrder);
|
const [columnOrder, saveColumnOrder] = useLocalStorage<string[]>('table-order', initialColumnOrder);
|
||||||
@@ -66,7 +64,9 @@ export default function Cuesheet({ data, columns, handleUpdate, selectedId }: Cu
|
|||||||
setColumnSizing({});
|
setColumnSizing({});
|
||||||
};
|
};
|
||||||
|
|
||||||
const headerGroups = table.getHeaderGroups;
|
const headerGroups = table.getHeaderGroups();
|
||||||
|
const rowModel = table.getRowModel();
|
||||||
|
const allLeafColumns = table.getAllLeafColumns();
|
||||||
|
|
||||||
let eventIndex = 0;
|
let eventIndex = 0;
|
||||||
let isPast = Boolean(selectedId);
|
let isPast = Boolean(selectedId);
|
||||||
@@ -75,7 +75,7 @@ export default function Cuesheet({ data, columns, handleUpdate, selectedId }: Cu
|
|||||||
<>
|
<>
|
||||||
{showSettings && (
|
{showSettings && (
|
||||||
<CuesheetTableSettings
|
<CuesheetTableSettings
|
||||||
columns={table.getAllLeafColumns()}
|
columns={allLeafColumns}
|
||||||
handleResetResizing={resetColumnResizing}
|
handleResetResizing={resetColumnResizing}
|
||||||
handleResetReordering={resetColumnOrder}
|
handleResetReordering={resetColumnOrder}
|
||||||
handleClearToggles={setAllVisible}
|
handleClearToggles={setAllVisible}
|
||||||
@@ -85,7 +85,7 @@ export default function Cuesheet({ data, columns, handleUpdate, selectedId }: Cu
|
|||||||
<table className={style.cuesheet}>
|
<table className={style.cuesheet}>
|
||||||
<CuesheetHeader headerGroups={headerGroups} />
|
<CuesheetHeader headerGroups={headerGroups} />
|
||||||
<tbody>
|
<tbody>
|
||||||
{table.getRowModel().rows.map((row) => {
|
{rowModel.rows.map((row) => {
|
||||||
const key = row.original.id;
|
const key = row.original.id;
|
||||||
const isSelected = selectedId === key;
|
const isSelected = selectedId === key;
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
@@ -121,8 +121,8 @@ export default function Cuesheet({ data, columns, handleUpdate, selectedId }: Cu
|
|||||||
} else if (row.original.colour) {
|
} else if (row.original.colour) {
|
||||||
try {
|
try {
|
||||||
// the colour is user defined and might be invalid
|
// the colour is user defined and might be invalid
|
||||||
const colour = new Color(row.original.colour).alpha(0.25);
|
const accessibleBackgroundColor = Color(getAccessibleColour(row.original.colour).backgroundColor);
|
||||||
rowBgColour = colour.hsl().string();
|
rowBgColour = accessibleBackgroundColor.fade(0.75).hexa();
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
/* we do not handle errors here */
|
/* we do not handle errors here */
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,6 @@
|
|||||||
|
|
||||||
& > * {
|
& > * {
|
||||||
border: 1px solid $white-10;
|
border: 1px solid $white-10;
|
||||||
border-radius: 4px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ import useRundown from '../../common/hooks-query/useRundown';
|
|||||||
import useUserFields from '../../common/hooks-query/useUserFields';
|
import useUserFields from '../../common/hooks-query/useUserFields';
|
||||||
import ExportModal, { ExportType } from '../modals/export-modal/ExportModal';
|
import ExportModal, { ExportType } from '../modals/export-modal/ExportModal';
|
||||||
|
|
||||||
|
import CuesheetProgress from './cuesheet-progress/CuesheetProgress';
|
||||||
import CuesheetTableHeader from './cuesheet-table-header/CuesheetTableHeader';
|
import CuesheetTableHeader from './cuesheet-table-header/CuesheetTableHeader';
|
||||||
import Cuesheet from './Cuesheet';
|
import Cuesheet from './Cuesheet';
|
||||||
import { makeCuesheetColumns } from './cuesheetCols';
|
import { makeCuesheetColumns } from './cuesheetCols';
|
||||||
@@ -140,6 +141,7 @@ export default function CuesheetWrapper() {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.tableWrapper} data-testid='cuesheet'>
|
<div className={styles.tableWrapper} data-testid='cuesheet'>
|
||||||
<CuesheetTableHeader handleExport={handleOpenModal} featureData={featureData} />
|
<CuesheetTableHeader handleExport={handleOpenModal} featureData={featureData} />
|
||||||
|
<CuesheetProgress />
|
||||||
<Cuesheet data={rundown} columns={columns} handleUpdate={handleUpdate} selectedId={featureData.selectedEventId} />
|
<Cuesheet data={rundown} columns={columns} handleUpdate={handleUpdate} selectedId={featureData.selectedEventId} />
|
||||||
<ExportModal isOpen={isModalOpen} onClose={onModalClose} />
|
<ExportModal isOpen={isModalOpen} onClose={onModalClose} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.progressOverride {
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import MultiPartProgressBar from '../../../common/components/multi-part-progress-bar/MultiPartProgressBar';
|
||||||
|
import { useTimer } from '../../../common/hooks/useSocket';
|
||||||
|
import useViewSettings from '../../../common/hooks-query/useViewSettings';
|
||||||
|
|
||||||
|
import styles from "./CuesheetProgress.module.scss"
|
||||||
|
|
||||||
|
export default function CuesheetProgress() {
|
||||||
|
const { data } = useViewSettings();
|
||||||
|
const timer = useTimer();
|
||||||
|
const totalTime = (timer.duration ?? 0) + (timer.addedTime ?? 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MultiPartProgressBar
|
||||||
|
now={timer.current}
|
||||||
|
complete={totalTime}
|
||||||
|
normalColor={data!.normalColor}
|
||||||
|
warning={data!.warningThreshold}
|
||||||
|
warningColor={data!.warningColor}
|
||||||
|
danger={data!.dangerThreshold}
|
||||||
|
dangerColor={data!.dangerColor}
|
||||||
|
className={styles.progressOverride}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ import { SortableCell } from './SortableCell';
|
|||||||
import style from '../Cuesheet.module.scss';
|
import style from '../Cuesheet.module.scss';
|
||||||
|
|
||||||
interface CuesheetHeaderProps {
|
interface CuesheetHeaderProps {
|
||||||
headerGroups: () => HeaderGroup<OntimeRundownEntry>[];
|
headerGroups: HeaderGroup<OntimeRundownEntry>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function CuesheetHeader(props: CuesheetHeaderProps) {
|
function CuesheetHeader(props: CuesheetHeaderProps) {
|
||||||
@@ -75,7 +75,7 @@ function CuesheetHeader(props: CuesheetHeaderProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<thead className={style.tableHeader}>
|
<thead className={style.tableHeader}>
|
||||||
{headerGroups().map((headerGroup) => {
|
{headerGroups.map((headerGroup) => {
|
||||||
const key = headerGroup.id;
|
const key = headerGroup.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ function EventRow(props: PropsWithChildren<EventRowProps>) {
|
|||||||
const ownRef = useRef<HTMLTableRowElement>(null);
|
const ownRef = useRef<HTMLTableRowElement>(null);
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
const bgColour = colour;
|
const textColour = getAccessibleColour(colour);
|
||||||
const textColour = getAccessibleColour(bgColour);
|
const bgColour = textColour.backgroundColor;
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver(
|
||||||
@@ -55,7 +55,7 @@ function EventRow(props: PropsWithChildren<EventRowProps>) {
|
|||||||
style={{ opacity: `${isPast ? pastOpacity : '1'}` }}
|
style={{ opacity: `${isPast ? pastOpacity : '1'}` }}
|
||||||
ref={selectedRef ?? ownRef}
|
ref={selectedRef ?? ownRef}
|
||||||
>
|
>
|
||||||
<td className={style.indexColumn} style={{ backgroundColor: bgColour, color: textColour?.color }}>
|
<td className={style.indexColumn} style={{ backgroundColor: bgColour, color: textColour.color }}>
|
||||||
{eventIndex}
|
{eventIndex}
|
||||||
</td>
|
</td>
|
||||||
{isVisible ? children : null}
|
{isVisible ? children : null}
|
||||||
|
|||||||
+10
-8
@@ -1,4 +1,4 @@
|
|||||||
import { memo } from 'react';
|
import { memo, ReactNode } from 'react';
|
||||||
import { Button, Checkbox, Switch } from '@chakra-ui/react';
|
import { Button, Checkbox, Switch } from '@chakra-ui/react';
|
||||||
import { Column } from '@tanstack/react-table';
|
import { Column } from '@tanstack/react-table';
|
||||||
import { OntimeRundownEntry } from 'ontime-types';
|
import { OntimeRundownEntry } from 'ontime-types';
|
||||||
@@ -22,12 +22,14 @@ interface CuesheetTableSettingsProps {
|
|||||||
|
|
||||||
function CuesheetTableSettings(props: CuesheetTableSettingsProps) {
|
function CuesheetTableSettings(props: CuesheetTableSettingsProps) {
|
||||||
const { columns, handleResetResizing, handleResetReordering, handleClearToggles } = props;
|
const { columns, handleResetResizing, handleResetReordering, handleClearToggles } = props;
|
||||||
const showPrevious = useCuesheetSettings((state) => state.showPrevious);
|
const {
|
||||||
const togglePreviousVisibility = useCuesheetSettings((state) => state.togglePreviousVisibility);
|
showPrevious,
|
||||||
const showDelayBlock = useCuesheetSettings((state) => state.showDelayBlock);
|
toggleDelayVisibility,
|
||||||
const toggleDelayVisibility = useCuesheetSettings((state) => state.toggleDelayVisibility);
|
showDelayBlock,
|
||||||
const showDelayedTimes = useCuesheetSettings((state) => state.showDelayedTimes);
|
showDelayedTimes,
|
||||||
const toggleDelayedTimes = useCuesheetSettings((state) => state.toggleDelayedTimes);
|
toggleDelayedTimes,
|
||||||
|
togglePreviousVisibility,
|
||||||
|
} = useCuesheetSettings();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.tableSettings}>
|
<div className={style.tableSettings}>
|
||||||
@@ -44,7 +46,7 @@ function CuesheetTableSettings(props: CuesheetTableSettingsProps) {
|
|||||||
defaultChecked={visible}
|
defaultChecked={visible}
|
||||||
onChange={column.getToggleVisibilityHandler()}
|
onChange={column.getToggleVisibilityHandler()}
|
||||||
/>
|
/>
|
||||||
{columnHeader}
|
{columnHeader as ReactNode}
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { millisToString } from 'ontime-utils';
|
|||||||
* @return {string}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const parseField = (field: keyof OntimeRundown, data: unknown): string => {
|
export const parseField = <T extends OntimeEntryCommonKeys>(field: T, data: unknown): string => {
|
||||||
let val;
|
let val;
|
||||||
switch (field) {
|
switch (field) {
|
||||||
case 'timeStart':
|
case 'timeStart':
|
||||||
@@ -96,6 +96,7 @@ export const makeTable = (headerData: ProjectData, rundown: OntimeRundown, userF
|
|||||||
|
|
||||||
rundown.forEach((entry) => {
|
rundown.forEach((entry) => {
|
||||||
const row: string[] = [];
|
const row: string[] = [];
|
||||||
|
// @ts-expect-error -- not sure how to type this
|
||||||
fieldOrder.forEach((field) => row.push(parseField(field, entry[field])));
|
fieldOrder.forEach((field) => row.push(parseField(field, entry[field])));
|
||||||
data.push(row);
|
data.push(row);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -73,8 +73,8 @@ export default function EventEditor() {
|
|||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
<CopyTag label='Event ID'>{event.id}</CopyTag>
|
<CopyTag label='Event ID'>{event.id}</CopyTag>
|
||||||
<CopyTag label='OSC trigger by id'>{`/ontime/gotoid/${event.id}`}</CopyTag>
|
<CopyTag label='OSC trigger by id'>{`/ontime/gotoid "${event.id}"`}</CopyTag>
|
||||||
<CopyTag label='OSC trigger by cue'>{`/ontime/gotocue/${event.cue}`}</CopyTag>
|
<CopyTag label='OSC trigger by cue'>{`/ontime/gotocue "${event.cue}"`}</CopyTag>
|
||||||
</EventEditorDataRight>
|
</EventEditorDataRight>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export default function CountedTextInput(props: CountedTextInputProps) {
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
|
autoComplete='off'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
font-size: $inner-section-text-size;
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -14,12 +15,12 @@
|
|||||||
|
|
||||||
.selected {
|
.selected {
|
||||||
min-width: max-content;
|
min-width: max-content;
|
||||||
font-size: $inner-section-text-size;
|
|
||||||
color: $label-gray;
|
color: $label-gray;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
|
font-size: $inner-section-text-size;
|
||||||
color: $label-gray;
|
color: $label-gray;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import { ModalBody, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/r
|
|||||||
|
|
||||||
import ModalWrapper from '../ModalWrapper';
|
import ModalWrapper from '../ModalWrapper';
|
||||||
|
|
||||||
import OscIntegration from './OscIntegration';
|
import HttpIntegration from './http/HttpIntegration';
|
||||||
import OscSettings from './OscSettings';
|
import OscIntegration from './osc/OscIntegration';
|
||||||
|
import OscSettings from './osc/OscSettings';
|
||||||
|
|
||||||
import styles from '../Modal.module.scss';
|
import styles from '../Modal.module.scss';
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ export default function IntegrationModal(props: IntegrationModalProps) {
|
|||||||
<TabList>
|
<TabList>
|
||||||
<Tab>OSC</Tab>
|
<Tab>OSC</Tab>
|
||||||
<Tab>OSC Integration</Tab>
|
<Tab>OSC Integration</Tab>
|
||||||
|
<Tab>HTTP Integration</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
@@ -38,6 +40,9 @@ export default function IntegrationModal(props: IntegrationModalProps) {
|
|||||||
<TabPanel>
|
<TabPanel>
|
||||||
<OscIntegration />
|
<OscIntegration />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
<TabPanel>
|
||||||
|
<HttpIntegration />
|
||||||
|
</TabPanel>
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { Switch } from '@chakra-ui/react';
|
||||||
|
import type { HttpSettings } from 'ontime-types';
|
||||||
|
import { TimerLifeCycle } from 'ontime-types';
|
||||||
|
|
||||||
|
import { useHttpSettings, usePostHttpSettings } from '../../../../common/hooks-query/useHttpSettings';
|
||||||
|
import { useEmitLog } from '../../../../common/stores/logger';
|
||||||
|
import ModalLoader from '../../modal-loader/ModalLoader';
|
||||||
|
import OntimeModalFooter from '../../OntimeModalFooter';
|
||||||
|
import { OntimeCycle, sectionText } from '../integration.utils';
|
||||||
|
|
||||||
|
import HttpSubscriptionRow from './HttpSubscriptionRow';
|
||||||
|
|
||||||
|
import styles from '../../Modal.module.scss';
|
||||||
|
|
||||||
|
export default function HttpIntegration() {
|
||||||
|
const { data, isFetching } = useHttpSettings();
|
||||||
|
const { mutateAsync } = usePostHttpSettings();
|
||||||
|
const { emitError } = useEmitLog();
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
register,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting, isDirty, isValid },
|
||||||
|
} = useForm<HttpSettings>({
|
||||||
|
mode: 'onBlur',
|
||||||
|
defaultValues: data,
|
||||||
|
values: data,
|
||||||
|
resetOptions: {
|
||||||
|
keepDirtyValues: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [showSection, setShowSection] = useState<OntimeCycle>(TimerLifeCycle.onLoad);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
reset(data);
|
||||||
|
}
|
||||||
|
}, [data, reset]);
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
reset(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (values: HttpSettings) => {
|
||||||
|
try {
|
||||||
|
const newSettings: HttpSettings = {
|
||||||
|
enabledOut: Boolean(values.enabledOut),
|
||||||
|
subscriptions: {
|
||||||
|
onLoad: values.subscriptions.onLoad ?? [],
|
||||||
|
onStart: values.subscriptions.onStart ?? [],
|
||||||
|
onPause: values.subscriptions.onPause ?? [],
|
||||||
|
onStop: values.subscriptions.onStop ?? [],
|
||||||
|
onUpdate: values.subscriptions.onUpdate ?? [],
|
||||||
|
onFinish: values.subscriptions.onFinish ?? [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await mutateAsync(newSettings);
|
||||||
|
} catch (error) {
|
||||||
|
emitError(`Error setting HTML: ${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isFetching) {
|
||||||
|
return <ModalLoader />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const placeholder = 'http://x.x.x.x:xxxx/api/path';
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer} id='http-subscriptions'>
|
||||||
|
<div className={styles.splitSection}>
|
||||||
|
<div>
|
||||||
|
<span className={`${styles.sectionTitle} ${styles.main}`}>HTTP Output</span>
|
||||||
|
<span className={styles.sectionSubtitle}>Ontime data feedback</span>
|
||||||
|
</div>
|
||||||
|
<Switch {...register('enabledOut')} variant='ontime-on-light' />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<HttpSubscriptionRow
|
||||||
|
cycle={TimerLifeCycle.onLoad}
|
||||||
|
title={sectionText.onLoad.title}
|
||||||
|
subtitle={sectionText.onLoad.subtitle}
|
||||||
|
visible={showSection === TimerLifeCycle.onLoad}
|
||||||
|
setShowSection={setShowSection}
|
||||||
|
register={register}
|
||||||
|
control={control}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
<HttpSubscriptionRow
|
||||||
|
cycle={TimerLifeCycle.onStart}
|
||||||
|
title={sectionText.onStart.title}
|
||||||
|
subtitle={sectionText.onStart.subtitle}
|
||||||
|
visible={showSection === TimerLifeCycle.onStart}
|
||||||
|
setShowSection={setShowSection}
|
||||||
|
register={register}
|
||||||
|
control={control}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
<HttpSubscriptionRow
|
||||||
|
cycle={TimerLifeCycle.onPause}
|
||||||
|
title={sectionText.onPause.title}
|
||||||
|
subtitle={sectionText.onPause.subtitle}
|
||||||
|
visible={showSection === TimerLifeCycle.onPause}
|
||||||
|
setShowSection={setShowSection}
|
||||||
|
register={register}
|
||||||
|
control={control}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
<HttpSubscriptionRow
|
||||||
|
cycle={TimerLifeCycle.onStop}
|
||||||
|
title={sectionText.onStop.title}
|
||||||
|
subtitle={sectionText.onStop.subtitle}
|
||||||
|
visible={showSection === TimerLifeCycle.onStop}
|
||||||
|
setShowSection={setShowSection}
|
||||||
|
register={register}
|
||||||
|
control={control}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
<HttpSubscriptionRow
|
||||||
|
cycle={TimerLifeCycle.onUpdate}
|
||||||
|
title={sectionText.onUpdate.title}
|
||||||
|
subtitle={sectionText.onUpdate.subtitle}
|
||||||
|
visible={showSection === TimerLifeCycle.onUpdate}
|
||||||
|
setShowSection={setShowSection}
|
||||||
|
register={register}
|
||||||
|
control={control}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
<HttpSubscriptionRow
|
||||||
|
cycle={TimerLifeCycle.onFinish}
|
||||||
|
title={sectionText.onFinish.title}
|
||||||
|
subtitle={sectionText.onFinish.subtitle}
|
||||||
|
visible={showSection === TimerLifeCycle.onFinish}
|
||||||
|
setShowSection={setShowSection}
|
||||||
|
register={register}
|
||||||
|
control={control}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
<OntimeModalFooter
|
||||||
|
formId='http-subscriptions'
|
||||||
|
handleRevert={resetForm}
|
||||||
|
isDirty={isDirty}
|
||||||
|
isValid={isValid}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { Control, useFieldArray, UseFormRegister } from 'react-hook-form';
|
||||||
|
import { Button, IconButton, Input, Switch } from '@chakra-ui/react';
|
||||||
|
import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp';
|
||||||
|
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
|
||||||
|
import { HttpSettings, TimerLifeCycle } from 'ontime-types';
|
||||||
|
|
||||||
|
import { useEmitLog } from '../../../../common/stores/logger';
|
||||||
|
import { startsWithHttp } from '../../../../common/utils/regex';
|
||||||
|
|
||||||
|
import collapseStyles from '../../../../common/components/collapse-bar/CollapseBar.module.scss';
|
||||||
|
import styles from '../../Modal.module.scss';
|
||||||
|
|
||||||
|
interface SubscriptionRowProps {
|
||||||
|
cycle: TimerLifeCycle;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
visible: boolean;
|
||||||
|
setShowSection: (cycle: TimerLifeCycle) => void;
|
||||||
|
register: UseFormRegister<HttpSettings>;
|
||||||
|
control: Control<HttpSettings>;
|
||||||
|
placeholder: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SubscriptionRow(props: SubscriptionRowProps) {
|
||||||
|
const { cycle, title, subtitle, visible, setShowSection, register, control, placeholder } = props;
|
||||||
|
const { emitError } = useEmitLog();
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
name: `subscriptions.${cycle}`,
|
||||||
|
control,
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasTooManyOptions = fields.length >= 3;
|
||||||
|
const headerStyle = `${styles.splitSection} ${visible ? '' : styles.showPointer}`;
|
||||||
|
|
||||||
|
const sectionTitle = `${title} ${fields.length ? fields.length : '-'} / 3`;
|
||||||
|
|
||||||
|
const handleAddNew = () => {
|
||||||
|
if (hasTooManyOptions) {
|
||||||
|
emitError(`Maximum amount of ${cycle} subscriptions reached (3)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
append({
|
||||||
|
message: '',
|
||||||
|
enabled: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={headerStyle} onClick={() => setShowSection(cycle)}>
|
||||||
|
<div>
|
||||||
|
<span className={`${styles.sectionTitle} ${styles.main}`}>{sectionTitle}</span>
|
||||||
|
{visible && <span className={styles.sectionSubtitle}>{subtitle}</span>}
|
||||||
|
</div>
|
||||||
|
<FiChevronUp className={visible ? collapseStyles.moreCollapsed : collapseStyles.moreExpanded} />
|
||||||
|
</div>
|
||||||
|
{visible && (
|
||||||
|
<>
|
||||||
|
{fields.map((subscription, index) => (
|
||||||
|
<div key={subscription.id} className={styles.entryRow}>
|
||||||
|
<IconButton
|
||||||
|
icon={<IoRemove />}
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
aria-label='delete'
|
||||||
|
size='xs'
|
||||||
|
colorScheme='red'
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder={placeholder}
|
||||||
|
size='xs'
|
||||||
|
variant='ontime-filled-on-light'
|
||||||
|
autoComplete='off'
|
||||||
|
{...register(`subscriptions.${cycle}.${index}.message`, {
|
||||||
|
pattern: { value: startsWithHttp, message: 'Request address must start with http://' },
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Switch variant='ontime-on-light' {...register(`subscriptions.${cycle}.${index}.enabled`)} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
onClick={handleAddNew}
|
||||||
|
className={styles.shiftRight}
|
||||||
|
isDisabled={hasTooManyOptions}
|
||||||
|
size='xs'
|
||||||
|
colorScheme='blue'
|
||||||
|
variant='outline'
|
||||||
|
padding='0 2em'
|
||||||
|
>
|
||||||
|
Add new
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { TimerLifeCycle } from 'ontime-types';
|
||||||
|
|
||||||
|
export type OntimeCycle = keyof typeof TimerLifeCycle;
|
||||||
|
|
||||||
|
export const sectionText: { [key in TimerLifeCycle]: { title: string; subtitle: string } } = {
|
||||||
|
onLoad: {
|
||||||
|
title: 'On Load',
|
||||||
|
subtitle: 'Triggers when a timer is loaded',
|
||||||
|
},
|
||||||
|
onStart: {
|
||||||
|
title: 'On Start',
|
||||||
|
subtitle: 'Triggers when a timer starts',
|
||||||
|
},
|
||||||
|
onPause: {
|
||||||
|
title: 'On Pause',
|
||||||
|
subtitle: 'Triggers when a running timer is paused',
|
||||||
|
},
|
||||||
|
onStop: {
|
||||||
|
title: 'On Stop',
|
||||||
|
subtitle: 'Triggers when a running timer is stopped',
|
||||||
|
},
|
||||||
|
onUpdate: {
|
||||||
|
title: 'On Every Second',
|
||||||
|
subtitle: 'Triggers when a running timer is updated (at least once a second, can be more)',
|
||||||
|
},
|
||||||
|
onFinish: {
|
||||||
|
title: 'On Finish',
|
||||||
|
subtitle: 'Triggers when a running reaches 0',
|
||||||
|
},
|
||||||
|
};
|
||||||
+13
-34
@@ -3,43 +3,15 @@ import { useForm } from 'react-hook-form';
|
|||||||
import type { OscSubscription } from 'ontime-types';
|
import type { OscSubscription } from 'ontime-types';
|
||||||
import { TimerLifeCycle } from 'ontime-types';
|
import { TimerLifeCycle } from 'ontime-types';
|
||||||
|
|
||||||
import useOscSettings, { usePostOscSubscriptions } from '../../../common/hooks-query/useOscSettings';
|
import useOscSettings, { usePostOscSubscriptions } from '../../../../common/hooks-query/useOscSettings';
|
||||||
import { useEmitLog } from '../../../common/stores/logger';
|
import { useEmitLog } from '../../../../common/stores/logger';
|
||||||
import ModalLoader from '../modal-loader/ModalLoader';
|
import ModalLoader from '../../modal-loader/ModalLoader';
|
||||||
import OntimeModalFooter from '../OntimeModalFooter';
|
import OntimeModalFooter from '../../OntimeModalFooter';
|
||||||
|
import { type OntimeCycle, sectionText } from '../integration.utils';
|
||||||
|
|
||||||
import OscSubscriptionRow from './OscSubscriptionRow';
|
import OscSubscriptionRow from './OscSubscriptionRow';
|
||||||
|
|
||||||
import styles from '../Modal.module.scss';
|
import styles from '../../Modal.module.scss';
|
||||||
|
|
||||||
type OntimeCycle = keyof typeof TimerLifeCycle;
|
|
||||||
|
|
||||||
const sectionText: { [key in TimerLifeCycle]: { title: string; subtitle: string } } = {
|
|
||||||
onLoad: {
|
|
||||||
title: 'On Load',
|
|
||||||
subtitle: 'Triggers when a timer is loaded',
|
|
||||||
},
|
|
||||||
onStart: {
|
|
||||||
title: 'On Start',
|
|
||||||
subtitle: 'Triggers when a timer starts',
|
|
||||||
},
|
|
||||||
onPause: {
|
|
||||||
title: 'On Pause',
|
|
||||||
subtitle: 'Triggers when a running timer is paused',
|
|
||||||
},
|
|
||||||
onStop: {
|
|
||||||
title: 'On Stop',
|
|
||||||
subtitle: 'Triggers when a running timer is stopped',
|
|
||||||
},
|
|
||||||
onUpdate: {
|
|
||||||
title: 'On Every Second',
|
|
||||||
subtitle: 'Triggers when a running timer is updated (at least once a second, can be more)',
|
|
||||||
},
|
|
||||||
onFinish: {
|
|
||||||
title: 'On Finish',
|
|
||||||
subtitle: 'Triggers when a running reaches 0',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function OscIntegration() {
|
export default function OscIntegration() {
|
||||||
const { data, isFetching } = useOscSettings();
|
const { data, isFetching } = useOscSettings();
|
||||||
@@ -92,6 +64,7 @@ export default function OscIntegration() {
|
|||||||
return <ModalLoader />;
|
return <ModalLoader />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const placeholder = 'OSC message';
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer} id='osc-subscriptions'>
|
<form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer} id='osc-subscriptions'>
|
||||||
<OscSubscriptionRow
|
<OscSubscriptionRow
|
||||||
@@ -102,6 +75,7 @@ export default function OscIntegration() {
|
|||||||
setShowSection={setShowSection}
|
setShowSection={setShowSection}
|
||||||
register={register}
|
register={register}
|
||||||
control={control}
|
control={control}
|
||||||
|
placeholder={placeholder}
|
||||||
/>
|
/>
|
||||||
<OscSubscriptionRow
|
<OscSubscriptionRow
|
||||||
cycle={TimerLifeCycle.onStart}
|
cycle={TimerLifeCycle.onStart}
|
||||||
@@ -111,6 +85,7 @@ export default function OscIntegration() {
|
|||||||
setShowSection={setShowSection}
|
setShowSection={setShowSection}
|
||||||
register={register}
|
register={register}
|
||||||
control={control}
|
control={control}
|
||||||
|
placeholder={placeholder}
|
||||||
/>
|
/>
|
||||||
<OscSubscriptionRow
|
<OscSubscriptionRow
|
||||||
cycle={TimerLifeCycle.onPause}
|
cycle={TimerLifeCycle.onPause}
|
||||||
@@ -120,6 +95,7 @@ export default function OscIntegration() {
|
|||||||
setShowSection={setShowSection}
|
setShowSection={setShowSection}
|
||||||
register={register}
|
register={register}
|
||||||
control={control}
|
control={control}
|
||||||
|
placeholder={placeholder}
|
||||||
/>
|
/>
|
||||||
<OscSubscriptionRow
|
<OscSubscriptionRow
|
||||||
cycle={TimerLifeCycle.onStop}
|
cycle={TimerLifeCycle.onStop}
|
||||||
@@ -129,6 +105,7 @@ export default function OscIntegration() {
|
|||||||
setShowSection={setShowSection}
|
setShowSection={setShowSection}
|
||||||
register={register}
|
register={register}
|
||||||
control={control}
|
control={control}
|
||||||
|
placeholder={placeholder}
|
||||||
/>
|
/>
|
||||||
<OscSubscriptionRow
|
<OscSubscriptionRow
|
||||||
cycle={TimerLifeCycle.onUpdate}
|
cycle={TimerLifeCycle.onUpdate}
|
||||||
@@ -138,6 +115,7 @@ export default function OscIntegration() {
|
|||||||
setShowSection={setShowSection}
|
setShowSection={setShowSection}
|
||||||
register={register}
|
register={register}
|
||||||
control={control}
|
control={control}
|
||||||
|
placeholder={placeholder}
|
||||||
/>
|
/>
|
||||||
<OscSubscriptionRow
|
<OscSubscriptionRow
|
||||||
cycle={TimerLifeCycle.onFinish}
|
cycle={TimerLifeCycle.onFinish}
|
||||||
@@ -147,6 +125,7 @@ export default function OscIntegration() {
|
|||||||
setShowSection={setShowSection}
|
setShowSection={setShowSection}
|
||||||
register={register}
|
register={register}
|
||||||
control={control}
|
control={control}
|
||||||
|
placeholder={placeholder}
|
||||||
/>
|
/>
|
||||||
<OntimeModalFooter
|
<OntimeModalFooter
|
||||||
formId='osc-subscriptions'
|
formId='osc-subscriptions'
|
||||||
+9
-7
@@ -1,15 +1,17 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-nocheck -- working on it
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { FormControl, Input, Switch } from '@chakra-ui/react';
|
import { FormControl, Input, Switch } from '@chakra-ui/react';
|
||||||
|
|
||||||
import useOscSettings, { useOscSettingsMutation } from '../../../common/hooks-query/useOscSettings';
|
import useOscSettings, { useOscSettingsMutation } from '../../../../common/hooks-query/useOscSettings';
|
||||||
import { PlaceholderSettings } from '../../../common/models/OscSettings';
|
import { PlaceholderSettings } from '../../../../common/models/OscSettings';
|
||||||
import { useEmitLog } from '../../../common/stores/logger';
|
import { useEmitLog } from '../../../../common/stores/logger';
|
||||||
import { isIPAddress, isOnlyNumbers } from '../../../common/utils/regex';
|
import { isIPAddress, isOnlyNumbers } from '../../../../common/utils/regex';
|
||||||
import ModalLoader from '../modal-loader/ModalLoader';
|
import ModalLoader from '../../modal-loader/ModalLoader';
|
||||||
import OntimeModalFooter from '../OntimeModalFooter';
|
import OntimeModalFooter from '../../OntimeModalFooter';
|
||||||
|
|
||||||
import styles from '../Modal.module.scss';
|
import styles from '../../Modal.module.scss';
|
||||||
|
|
||||||
export default function OscSettings() {
|
export default function OscSettings() {
|
||||||
const { data, isFetching } = useOscSettings();
|
const { data, isFetching } = useOscSettings();
|
||||||
+7
-6
@@ -2,12 +2,12 @@ import { Control, useFieldArray, UseFormRegister } from 'react-hook-form';
|
|||||||
import { Button, IconButton, Input, Switch } from '@chakra-ui/react';
|
import { Button, IconButton, Input, Switch } from '@chakra-ui/react';
|
||||||
import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp';
|
import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp';
|
||||||
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
|
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
|
||||||
import { OscSubscription, TimerLifeCycle } from 'ontime-types';
|
import type { OscSubscription, TimerLifeCycle } from 'ontime-types';
|
||||||
|
|
||||||
import { useEmitLog } from '../../../common/stores/logger';
|
import { useEmitLog } from '../../../../common/stores/logger';
|
||||||
|
|
||||||
import collapseStyles from '../../../common/components/collapse-bar/CollapseBar.module.scss';
|
import collapseStyles from '../../../../common/components/collapse-bar/CollapseBar.module.scss';
|
||||||
import styles from '../Modal.module.scss';
|
import styles from '../../Modal.module.scss';
|
||||||
|
|
||||||
interface OscSubscriptionRowProps {
|
interface OscSubscriptionRowProps {
|
||||||
cycle: TimerLifeCycle;
|
cycle: TimerLifeCycle;
|
||||||
@@ -17,10 +17,11 @@ interface OscSubscriptionRowProps {
|
|||||||
setShowSection: (cycle: TimerLifeCycle) => void;
|
setShowSection: (cycle: TimerLifeCycle) => void;
|
||||||
register: UseFormRegister<OscSubscription>;
|
register: UseFormRegister<OscSubscription>;
|
||||||
control: Control<OscSubscription>;
|
control: Control<OscSubscription>;
|
||||||
|
placeholder: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function OscSubscriptionRow(props: OscSubscriptionRowProps) {
|
export default function OscSubscriptionRow(props: OscSubscriptionRowProps) {
|
||||||
const { cycle, title, subtitle, visible, setShowSection, register, control } = props;
|
const { cycle, title, subtitle, visible, setShowSection, register, control, placeholder } = props;
|
||||||
const { emitError } = useEmitLog();
|
const { emitError } = useEmitLog();
|
||||||
const { fields, append, remove } = useFieldArray({
|
const { fields, append, remove } = useFieldArray({
|
||||||
name: cycle,
|
name: cycle,
|
||||||
@@ -64,7 +65,7 @@ export default function OscSubscriptionRow(props: OscSubscriptionRowProps) {
|
|||||||
colorScheme='red'
|
colorScheme='red'
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder='OSC Message'
|
placeholder={placeholder}
|
||||||
size='xs'
|
size='xs'
|
||||||
variant='ontime-filled-on-light'
|
variant='ontime-filled-on-light'
|
||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import type { ProjectData } from 'ontime-types';
|
import type { ProjectData } from 'ontime-types';
|
||||||
|
|
||||||
import { PROJECT_DATA, RUNDOWN_TABLE } from '../../../common/api/apiConstants';
|
import { PROJECT_DATA, RUNDOWN } from '../../../common/api/apiConstants';
|
||||||
import { postNew } from '../../../common/api/ontimeApi';
|
import { postNew } from '../../../common/api/ontimeApi';
|
||||||
import useProjectData from '../../../common/hooks-query/useProjectData';
|
import useProjectData from '../../../common/hooks-query/useProjectData';
|
||||||
import { projectDataPlaceholder } from '../../../common/models/ProjectData';
|
import { projectDataPlaceholder } from '../../../common/models/ProjectData';
|
||||||
@@ -52,8 +52,8 @@ export default function QuickStart({ onClose, isOpen }: QuickStartProps) {
|
|||||||
const onSubmit = async (data: Partial<ProjectData>) => {
|
const onSubmit = async (data: Partial<ProjectData>) => {
|
||||||
try {
|
try {
|
||||||
await postNew(data);
|
await postNew(data);
|
||||||
await ontimeQueryClient.invalidateQueries(PROJECT_DATA);
|
await ontimeQueryClient.invalidateQueries({ queryKey: PROJECT_DATA });
|
||||||
await ontimeQueryClient.invalidateQueries(RUNDOWN_TABLE);
|
await ontimeQueryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default function AliasesForm() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
reset(data);
|
reset({ aliases: data });
|
||||||
}
|
}
|
||||||
}, [data, reset]);
|
}, [data, reset]);
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ export default function AliasesForm() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const disableInputs = status === 'loading';
|
const disableInputs = status === 'pending';
|
||||||
const hasTooManyOptions = fields.length >= 20;
|
const hasTooManyOptions = fields.length >= 20;
|
||||||
|
|
||||||
if (isFetching) {
|
if (isFetching) {
|
||||||
@@ -112,6 +112,7 @@ export default function AliasesForm() {
|
|||||||
icon={<IoRemove />}
|
icon={<IoRemove />}
|
||||||
colorScheme='red'
|
colorScheme='red'
|
||||||
isDisabled={disableInputs}
|
isDisabled={disableInputs}
|
||||||
|
data-testid={`field__delete_${index}`}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
@@ -121,6 +122,7 @@ export default function AliasesForm() {
|
|||||||
variant='ontime-filled-on-light'
|
variant='ontime-filled-on-light'
|
||||||
placeholder='URL Alias'
|
placeholder='URL Alias'
|
||||||
isDisabled={disableInputs}
|
isDisabled={disableInputs}
|
||||||
|
data-testid={`field__alias_${index}`}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
@@ -130,6 +132,7 @@ export default function AliasesForm() {
|
|||||||
variant='ontime-filled-on-light'
|
variant='ontime-filled-on-light'
|
||||||
placeholder='URL (portion after ontime Port)'
|
placeholder='URL (portion after ontime Port)'
|
||||||
isDisabled={disableInputs}
|
isDisabled={disableInputs}
|
||||||
|
data-testid={`field__url_${index}`}
|
||||||
/>
|
/>
|
||||||
<TooltipActionBtn
|
<TooltipActionBtn
|
||||||
clickHandler={(event) => handleLinks(event, alias.alias)}
|
clickHandler={(event) => handleLinks(event, alias.alias)}
|
||||||
@@ -140,8 +143,14 @@ export default function AliasesForm() {
|
|||||||
icon={<IoOpenOutline />}
|
icon={<IoOpenOutline />}
|
||||||
colorScheme='red'
|
colorScheme='red'
|
||||||
isDisabled={disableInputs}
|
isDisabled={disableInputs}
|
||||||
|
data-testid={`field__test_${index}`}
|
||||||
|
/>
|
||||||
|
<Switch
|
||||||
|
{...register(`aliases.${index}.enabled`)}
|
||||||
|
variant='ontime-on-light'
|
||||||
|
isDisabled={disableInputs}
|
||||||
|
data-testid={`field__enable_${index}`}
|
||||||
/>
|
/>
|
||||||
<Switch {...register(`aliases.${index}.enabled`)} variant='ontime-on-light' isDisabled={disableInputs} />
|
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export default function AppSettingsModal() {
|
|||||||
reset(data);
|
reset(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const disableInputs = status === 'loading';
|
const disableInputs = status === 'pending';
|
||||||
|
|
||||||
if (isFetching) {
|
if (isFetching) {
|
||||||
return <ModalLoader />;
|
return <ModalLoader />;
|
||||||
@@ -87,7 +87,7 @@ export default function AppSettingsModal() {
|
|||||||
description='Protect the editor with a pin code'
|
description='Protect the editor with a pin code'
|
||||||
error={errors.editorKey?.message}
|
error={errors.editorKey?.message}
|
||||||
>
|
>
|
||||||
<ModalPinInput register={register} formName='editorKey' isDisabled={disableInputs} />
|
<ModalPinInput register={register as any} formName='editorKey' isDisabled={disableInputs} />
|
||||||
</ModalSplitInput>
|
</ModalSplitInput>
|
||||||
<ModalSplitInput
|
<ModalSplitInput
|
||||||
field='operatorKey'
|
field='operatorKey'
|
||||||
@@ -95,7 +95,7 @@ export default function AppSettingsModal() {
|
|||||||
description='Protect the cuesheet with a pin code'
|
description='Protect the cuesheet with a pin code'
|
||||||
error={errors.operatorKey?.message}
|
error={errors.operatorKey?.message}
|
||||||
>
|
>
|
||||||
<ModalPinInput register={register} formName='operatorKey' isDisabled={disableInputs} />
|
<ModalPinInput register={register as any} formName='operatorKey' isDisabled={disableInputs} />
|
||||||
</ModalSplitInput>
|
</ModalSplitInput>
|
||||||
<div style={{ height: '16px' }} />
|
<div style={{ height: '16px' }} />
|
||||||
<ModalSplitInput
|
<ModalSplitInput
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export default function CuesheetSettingsForm() {
|
|||||||
reset(data);
|
reset(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const disableInputs = status === 'loading';
|
const disableInputs = status === 'pending';
|
||||||
|
|
||||||
if (isFetching) {
|
if (isFetching) {
|
||||||
return <ModalLoader />;
|
return <ModalLoader />;
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default function ProjectDataForm() {
|
|||||||
reset(data);
|
reset(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const disableInputs = status === 'loading';
|
const disableInputs = status === 'pending';
|
||||||
|
|
||||||
if (isFetching) {
|
if (isFetching) {
|
||||||
return <ModalLoader />;
|
return <ModalLoader />;
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export default function ViewSettingsForm() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const disableInputs = status === 'loading';
|
const disableInputs = status === 'pending';
|
||||||
|
|
||||||
if (isFetching || isFetchingInfo) {
|
if (isFetching || isFetchingInfo) {
|
||||||
return <ModalLoader />;
|
return <ModalLoader />;
|
||||||
@@ -94,7 +94,7 @@ export default function ViewSettingsForm() {
|
|||||||
<AlertTitle>CSS Override</AlertTitle>
|
<AlertTitle>CSS Override</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
Ontime will use the CSS file at its install location. <br />
|
Ontime will use the CSS file at its install location. <br />
|
||||||
<span className={style.url}>{info.cssOverride}</span>
|
<span className={style.url}>{info?.cssOverride}</span>
|
||||||
<ModalLink href={cssOverrideDocsUrl}>For more information, see the docs</ModalLink>
|
<ModalLink href={cssOverrideDocsUrl}>For more information, see the docs</ModalLink>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { useQueryClient } from '@tanstack/react-query';
|
|||||||
import { OntimeRundown, ProjectData, UserFields } from 'ontime-types';
|
import { OntimeRundown, ProjectData, UserFields } from 'ontime-types';
|
||||||
import { defaultExcelImportMap, ExcelImportMap } from 'ontime-utils';
|
import { defaultExcelImportMap, ExcelImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
import { PROJECT_DATA, RUNDOWN_TABLE, USERFIELDS } from '../../../common/api/apiConstants';
|
import { PROJECT_DATA, RUNDOWN, USERFIELDS } from '../../../common/api/apiConstants';
|
||||||
import { invalidateAllCaches, maybeAxiosError } from '../../../common/api/apiUtils';
|
import { invalidateAllCaches, maybeAxiosError } from '../../../common/api/apiUtils';
|
||||||
import {
|
import {
|
||||||
patchData,
|
patchData,
|
||||||
@@ -155,11 +155,11 @@ export default function UploadModal({ onClose, isOpen }: UploadModalProps) {
|
|||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
await patchData({ rundown, userFields, project });
|
await patchData({ rundown, userFields, project });
|
||||||
queryClient.setQueryData(RUNDOWN_TABLE, rundown);
|
queryClient.setQueryData(RUNDOWN, { rundown, revision: -1 });
|
||||||
queryClient.setQueryData(USERFIELDS, userFields);
|
queryClient.setQueryData(USERFIELDS, userFields);
|
||||||
queryClient.setQueryData(PROJECT_DATA, project);
|
queryClient.setQueryData(PROJECT_DATA, project);
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: [...RUNDOWN_TABLE, ...USERFIELDS, ...PROJECT_DATA],
|
queryKey: [...RUNDOWN, ...USERFIELDS, ...PROJECT_DATA],
|
||||||
});
|
});
|
||||||
doClose = true;
|
doClose = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -22,3 +22,24 @@
|
|||||||
.spacer {
|
.spacer {
|
||||||
min-height: 95vh;
|
min-height: 95vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editPrompt {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
background: rgba(black, 0.6);
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 0.5em 2em;
|
||||||
|
color: gold;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
transition-property: opacity;
|
||||||
|
transition-duration: 0.3s;
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { isOntimeEvent, OntimeEvent, SupportedEvent, UserFields } from 'ontime-types';
|
import { isOntimeEvent, OntimeEvent, SupportedEvent, UserFields } from 'ontime-types';
|
||||||
import { getFirstEvent, getLastEvent } from 'ontime-utils';
|
import { getFirstEvent, getLastEvent } from 'ontime-utils';
|
||||||
@@ -11,9 +11,12 @@ import useFollowComponent from '../../common/hooks/useFollowComponent';
|
|||||||
import { useOperator } from '../../common/hooks/useSocket';
|
import { useOperator } from '../../common/hooks/useSocket';
|
||||||
import useProjectData from '../../common/hooks-query/useProjectData';
|
import useProjectData from '../../common/hooks-query/useProjectData';
|
||||||
import useRundown from '../../common/hooks-query/useRundown';
|
import useRundown from '../../common/hooks-query/useRundown';
|
||||||
|
import useSettings from '../../common/hooks-query/useSettings';
|
||||||
import useUserFields from '../../common/hooks-query/useUserFields';
|
import useUserFields from '../../common/hooks-query/useUserFields';
|
||||||
|
import { debounce } from '../../common/utils/debounce';
|
||||||
import { isStringBoolean } from '../../common/utils/viewUtils';
|
import { isStringBoolean } from '../../common/utils/viewUtils';
|
||||||
|
|
||||||
|
import EditModal from './edit-modal/EditModal';
|
||||||
import FollowButton from './follow-button/FollowButton';
|
import FollowButton from './follow-button/FollowButton';
|
||||||
import OperatorBlock from './operator-block/OperatorBlock';
|
import OperatorBlock from './operator-block/OperatorBlock';
|
||||||
import OperatorEvent from './operator-event/OperatorEvent';
|
import OperatorEvent from './operator-event/OperatorEvent';
|
||||||
@@ -24,15 +27,25 @@ import style from './Operator.module.scss';
|
|||||||
const selectedOffset = 50;
|
const selectedOffset = 50;
|
||||||
|
|
||||||
type TitleFields = Pick<OntimeEvent, 'title' | 'subtitle' | 'presenter'>;
|
type TitleFields = Pick<OntimeEvent, 'title' | 'subtitle' | 'presenter'>;
|
||||||
|
export type EditEvent = Pick<OntimeEvent, 'id' | 'cue'> & { fieldLabel?: string; fieldValue: string };
|
||||||
|
export type PartialEdit = EditEvent & {
|
||||||
|
field: keyof UserFields;
|
||||||
|
};
|
||||||
|
|
||||||
export default function Operator() {
|
export default function Operator() {
|
||||||
const { data, status } = useRundown();
|
const { data, status } = useRundown();
|
||||||
const { data: userFields, status: userFieldsStatus } = useUserFields();
|
const { data: userFields, status: userFieldsStatus } = useUserFields();
|
||||||
const { data: projectData, status: projectDataStatus } = useProjectData();
|
const { data: projectData, status: projectDataStatus } = useProjectData();
|
||||||
|
|
||||||
|
const timeoutId = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
const featureData = useOperator();
|
const featureData = useOperator();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
const { data: settings } = useSettings();
|
||||||
|
|
||||||
|
const [showEditPrompt, setShowEditPrompt] = useState(false);
|
||||||
|
const [editEvent, setEditEvent] = useState<PartialEdit | null>(null);
|
||||||
|
|
||||||
const isAutomatedScroll = useRef(false);
|
|
||||||
const [lockAutoScroll, setLockAutoScroll] = useState(false);
|
const [lockAutoScroll, setLockAutoScroll] = useState(false);
|
||||||
const selectedRef = useRef<HTMLDivElement | null>(null);
|
const selectedRef = useRef<HTMLDivElement | null>(null);
|
||||||
const scrollRef = useRef<HTMLDivElement | null>(null);
|
const scrollRef = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -41,7 +54,6 @@ export default function Operator() {
|
|||||||
scrollRef: scrollRef,
|
scrollRef: scrollRef,
|
||||||
doFollow: !lockAutoScroll,
|
doFollow: !lockAutoScroll,
|
||||||
topOffset: selectedOffset,
|
topOffset: selectedOffset,
|
||||||
setScrollFlag: () => (isAutomatedScroll.current = true),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set window title
|
// Set window title
|
||||||
@@ -65,13 +77,8 @@ export default function Operator() {
|
|||||||
setLockAutoScroll(false);
|
setLockAutoScroll(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleScroll = () => {
|
// prevent considering automated scrolls as user scrolls
|
||||||
// prevent considering automated scrolls as user scrolls
|
const handleUserScroll = () => {
|
||||||
if (isAutomatedScroll.current) {
|
|
||||||
isAutomatedScroll.current = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedRef?.current && scrollRef?.current) {
|
if (selectedRef?.current && scrollRef?.current) {
|
||||||
const selectedRect = selectedRef.current.getBoundingClientRect();
|
const selectedRect = selectedRef.current.getBoundingClientRect();
|
||||||
const scrollerRect = scrollRef.current.getBoundingClientRect();
|
const scrollerRect = scrollRef.current.getBoundingClientRect();
|
||||||
@@ -82,9 +89,34 @@ export default function Operator() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const debouncedHandleScroll = debounce(handleUserScroll, 1000);
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (timeoutId.current) {
|
||||||
|
clearTimeout(timeoutId.current);
|
||||||
|
}
|
||||||
|
timeoutId.current = setTimeout(() => {
|
||||||
|
setShowEditPrompt(false);
|
||||||
|
}, 700);
|
||||||
|
|
||||||
|
setShowEditPrompt(true);
|
||||||
|
|
||||||
|
debouncedHandleScroll();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = useCallback(
|
||||||
|
(event: EditEvent) => {
|
||||||
|
const field = searchParams.get('subscribe') as keyof UserFields | null;
|
||||||
|
|
||||||
|
if (field) {
|
||||||
|
setEditEvent({ ...event, field });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[searchParams],
|
||||||
|
);
|
||||||
|
|
||||||
const missingData = !data || !userFields || !projectData;
|
const missingData = !data || !userFields || !projectData;
|
||||||
const isLoading = status === 'loading' || userFieldsStatus === 'loading' || projectDataStatus === 'loading';
|
const isLoading = status === 'pending' || userFieldsStatus === 'pending' || projectDataStatus === 'pending';
|
||||||
|
|
||||||
if (missingData || isLoading) {
|
if (missingData || isLoading) {
|
||||||
return <Empty text='Loading...' />;
|
return <Empty text='Loading...' />;
|
||||||
@@ -97,7 +129,7 @@ export default function Operator() {
|
|||||||
const subscribedAlias = subscribe ? userFields[subscribe] : '';
|
const subscribedAlias = subscribe ? userFields[subscribe] : '';
|
||||||
const showSeconds = isStringBoolean(searchParams.get('showseconds'));
|
const showSeconds = isStringBoolean(searchParams.get('showseconds'));
|
||||||
|
|
||||||
const operatorOptions = getOperatorOptions(userFields);
|
const operatorOptions = getOperatorOptions(userFields, settings?.timeFormat ?? '24');
|
||||||
let isPast = Boolean(featureData.selectedEventId);
|
let isPast = Boolean(featureData.selectedEventId);
|
||||||
const hidePast = isStringBoolean(searchParams.get('hidepast'));
|
const hidePast = isStringBoolean(searchParams.get('hidepast'));
|
||||||
|
|
||||||
@@ -108,6 +140,7 @@ export default function Operator() {
|
|||||||
<div className={style.operatorContainer}>
|
<div className={style.operatorContainer}>
|
||||||
<NavigationMenu />
|
<NavigationMenu />
|
||||||
<ViewParamsEditor paramFields={operatorOptions} />
|
<ViewParamsEditor paramFields={operatorOptions} />
|
||||||
|
{editEvent && <EditModal event={editEvent} onClose={() => setEditEvent(null)} />}
|
||||||
|
|
||||||
<StatusBar
|
<StatusBar
|
||||||
projectTitle={projectData.title}
|
projectTitle={projectData.title}
|
||||||
@@ -119,7 +152,13 @@ export default function Operator() {
|
|||||||
lastId={lastEvent?.id}
|
lastId={lastEvent?.id}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={style.operatorEvents} onScroll={handleScroll} ref={scrollRef}>
|
{subscribe && (
|
||||||
|
<div className={`${style.editPrompt} ${showEditPrompt ? style.show : undefined}`}>
|
||||||
|
Press and hold to edit user field
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={style.operatorEvents} onWheel={handleScroll} onTouchMove={handleScroll} ref={scrollRef}>
|
||||||
{data.map((entry) => {
|
{data.map((entry) => {
|
||||||
if (isOntimeEvent(entry)) {
|
if (isOntimeEvent(entry)) {
|
||||||
const isSelected = featureData.selectedEventId === entry.id;
|
const isSelected = featureData.selectedEventId === entry.id;
|
||||||
@@ -139,6 +178,7 @@ export default function Operator() {
|
|||||||
return (
|
return (
|
||||||
<OperatorEvent
|
<OperatorEvent
|
||||||
key={entry.id}
|
key={entry.id}
|
||||||
|
id={entry.id}
|
||||||
colour={entry.colour}
|
colour={entry.colour}
|
||||||
cue={entry.cue}
|
cue={entry.cue}
|
||||||
main={mainField}
|
main={mainField}
|
||||||
@@ -153,6 +193,7 @@ export default function Operator() {
|
|||||||
showSeconds={showSeconds}
|
showSeconds={showSeconds}
|
||||||
isPast={isPast}
|
isPast={isPast}
|
||||||
selectedRef={isSelected ? selectedRef : undefined}
|
selectedRef={isSelected ? selectedRef : undefined}
|
||||||
|
onLongPress={subscribe ? handleEdit : () => undefined}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
@use '../../../theme/v2Styles' as *;
|
||||||
|
@use '../../../theme/ontimeColours' as *;
|
||||||
|
|
||||||
|
.editModal {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
margin: 0 auto;
|
||||||
|
top: 20%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: $gray-1250;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
min-width: min(400px, 90vw);
|
||||||
|
box-shadow: $box-shadow-l1;
|
||||||
|
|
||||||
|
.buttonRow {
|
||||||
|
margin-top: auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { Button, Textarea } from '@chakra-ui/react';
|
||||||
|
import { OntimeEvent } from 'ontime-types';
|
||||||
|
|
||||||
|
import { useEventAction } from '../../../common/hooks/useEventAction';
|
||||||
|
import type { PartialEdit } from '../Operator';
|
||||||
|
|
||||||
|
import style from './EditModal.module.scss';
|
||||||
|
|
||||||
|
interface EditModalProps {
|
||||||
|
event: PartialEdit;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EditModal(props: EditModalProps) {
|
||||||
|
const { event, onClose } = props;
|
||||||
|
|
||||||
|
const { updateEvent } = useEventAction();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const newValue = inputRef.current?.value;
|
||||||
|
|
||||||
|
const partialEvent: Partial<OntimeEvent> = {
|
||||||
|
id: event.id,
|
||||||
|
[event.field]: newValue,
|
||||||
|
};
|
||||||
|
await updateEvent(partialEvent);
|
||||||
|
setLoading(false);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const fieldLabel = event?.fieldLabel ?? event.field;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={style.editModal}>
|
||||||
|
<div>{`Editing field ${fieldLabel} in cue ${event.cue}`}</div>
|
||||||
|
<Textarea
|
||||||
|
ref={inputRef}
|
||||||
|
variant='ontime-filled'
|
||||||
|
placeholder={`Add value for ${fieldLabel} field`}
|
||||||
|
defaultValue={event.fieldValue}
|
||||||
|
isDisabled={loading}
|
||||||
|
/>
|
||||||
|
<div className={style.buttonRow}>
|
||||||
|
<Button variant='ontime-subtle' onClick={onClose} isDisabled={loading}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant='ontime-filled' onClick={handleSave} isDisabled={loading}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,9 +2,11 @@
|
|||||||
@use '../../../../src/theme/v2Styles' as *;
|
@use '../../../../src/theme/v2Styles' as *;
|
||||||
|
|
||||||
.followButton {
|
.followButton {
|
||||||
position: relative;
|
position: fixed;
|
||||||
bottom: 12rem;
|
bottom: 1.5rem;
|
||||||
margin: 0 auto;
|
bottom: calc(1.5rem + env(safe-area-inset-bottom));
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { memo, RefObject } from 'react';
|
import { memo, RefObject, SyntheticEvent } from 'react';
|
||||||
|
|
||||||
import DelayIndicator from '../../../common/components/delay-indicator/DelayIndicator';
|
import DelayIndicator from '../../../common/components/delay-indicator/DelayIndicator';
|
||||||
|
import useLongPress from '../../../common/hooks/useLongPress';
|
||||||
import { useTimer } from '../../../common/hooks/useSocket';
|
import { useTimer } from '../../../common/hooks/useSocket';
|
||||||
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
|
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
|
||||||
import { formatTime } from '../../../common/utils/time';
|
import { formatTime } from '../../../common/utils/time';
|
||||||
|
import type { EditEvent } from '../Operator';
|
||||||
|
|
||||||
import style from './OperatorEvent.module.scss';
|
import style from './OperatorEvent.module.scss';
|
||||||
|
|
||||||
interface OperatorEventProps {
|
interface OperatorEventProps {
|
||||||
|
id: string;
|
||||||
colour: string;
|
colour: string;
|
||||||
cue: string;
|
cue: string;
|
||||||
main: string;
|
main: string;
|
||||||
@@ -22,6 +25,7 @@ interface OperatorEventProps {
|
|||||||
showSeconds: boolean;
|
showSeconds: boolean;
|
||||||
isPast: boolean;
|
isPast: boolean;
|
||||||
selectedRef?: RefObject<HTMLDivElement>;
|
selectedRef?: RefObject<HTMLDivElement>;
|
||||||
|
onLongPress: (event: EditEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// extract this to contain re-renders
|
// extract this to contain re-renders
|
||||||
@@ -32,6 +36,7 @@ function RollingTime() {
|
|||||||
|
|
||||||
function OperatorEvent(props: OperatorEventProps) {
|
function OperatorEvent(props: OperatorEventProps) {
|
||||||
const {
|
const {
|
||||||
|
id,
|
||||||
colour,
|
colour,
|
||||||
cue,
|
cue,
|
||||||
main,
|
main,
|
||||||
@@ -46,8 +51,17 @@ function OperatorEvent(props: OperatorEventProps) {
|
|||||||
showSeconds,
|
showSeconds,
|
||||||
isPast,
|
isPast,
|
||||||
selectedRef,
|
selectedRef,
|
||||||
|
onLongPress,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const handleLongPress = (event?: SyntheticEvent) => {
|
||||||
|
// we dont have an event out of useLongPress
|
||||||
|
event?.preventDefault();
|
||||||
|
onLongPress({ id, cue, fieldLabel: subscribedAlias, fieldValue: subscribed ?? '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const mouseHandlers = useLongPress(handleLongPress, { threshold: 800 });
|
||||||
|
|
||||||
const start = formatTime(timeStart, { showSeconds });
|
const start = formatTime(timeStart, { showSeconds });
|
||||||
const end = formatTime(timeEnd, { showSeconds });
|
const end = formatTime(timeEnd, { showSeconds });
|
||||||
|
|
||||||
@@ -61,7 +75,7 @@ function OperatorEvent(props: OperatorEventProps) {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={operatorClasses} ref={selectedRef}>
|
<div className={operatorClasses} ref={selectedRef} onContextMenu={handleLongPress} {...mouseHandlers}>
|
||||||
<div className={style.binder} style={{ ...cueColours }}>
|
<div className={style.binder} style={{ ...cueColours }}>
|
||||||
<span className={style.cue}>{cue}</span>
|
<span className={style.cue}>{cue}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,16 +15,19 @@
|
|||||||
background-color: $gray-1350;
|
background-color: $gray-1350;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-bottom: 1px solid $white-10;
|
border-bottom: 1px solid $white-10;
|
||||||
box-shadow: $large-top-drawer-shadow;
|
box-shadow: $large-top-drawer-shadow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timers {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"playback timer1B timer2B timer3B";
|
"playback timer1B timer2B timer3B";
|
||||||
grid-template-columns: 1fr auto auto auto;
|
grid-template-columns: 1fr auto auto auto;
|
||||||
column-gap: 1.5rem;
|
column-gap: 1.5rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.playbackIcon {
|
.playbackIcon {
|
||||||
@@ -83,7 +86,7 @@
|
|||||||
|
|
||||||
// tablet
|
// tablet
|
||||||
@media (min-width: $min-tablet) {
|
@media (min-width: $min-tablet) {
|
||||||
.statusBar {
|
.timers {
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"playback timer1B timer2A timer3A"
|
"playback timer1B timer2A timer3A"
|
||||||
"title title timer2B timer3B";
|
"title title timer2B timer3B";
|
||||||
@@ -103,3 +106,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.progressOverride {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
import { Playback } from 'ontime-types';
|
import { Playback } from 'ontime-types';
|
||||||
import { millisToString } from 'ontime-utils';
|
|
||||||
|
|
||||||
import PlaybackIcon from '../../../common/components/playback-icon/PlaybackIcon';
|
import useViewSettings from '../../../common/hooks-query/useViewSettings';
|
||||||
import { useTimer } from '../../../common/hooks/useSocket';
|
|
||||||
import { cx } from '../../../common/utils/styleUtils';
|
import StatusBarProgress from './StatusBarProgress';
|
||||||
import { formatTime } from '../../../common/utils/time';
|
import StatusBarTimers from './StatusBarTimers';
|
||||||
|
|
||||||
import styles from './StatusBar.module.scss';
|
import styles from './StatusBar.module.scss';
|
||||||
|
|
||||||
@@ -22,73 +20,20 @@ interface StatusBarProps {
|
|||||||
export default function StatusBar(props: StatusBarProps) {
|
export default function StatusBar(props: StatusBarProps) {
|
||||||
const { projectTitle, playback, selectedEventId, firstStart, firstId, lastEnd, lastId } = props;
|
const { projectTitle, playback, selectedEventId, firstStart, firstId, lastEnd, lastId } = props;
|
||||||
|
|
||||||
const timer = useTimer();
|
const { data } = useViewSettings();
|
||||||
|
|
||||||
const getTimeStart = () => {
|
|
||||||
if (firstStart === undefined) {
|
|
||||||
return '...';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedEventId) {
|
|
||||||
if (firstId === selectedEventId) {
|
|
||||||
return millisToString(timer.expectedFinish);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return millisToString(firstStart);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTimeEnd = () => {
|
|
||||||
if (lastEnd === undefined) {
|
|
||||||
return '...';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedEventId) {
|
|
||||||
if (lastId === selectedEventId) {
|
|
||||||
return millisToString(timer.expectedFinish);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return millisToString(lastEnd);
|
|
||||||
};
|
|
||||||
|
|
||||||
// use user defined format
|
|
||||||
const timeNow = formatTime(timer.clock, {
|
|
||||||
showSeconds: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const runningTime = millisToString(timer.current);
|
|
||||||
const elapsedTime = millisToString(timer.elapsed);
|
|
||||||
|
|
||||||
const PlaybackIconComponent = useMemo(() => {
|
|
||||||
const isPlaying = playback === Playback.Play || playback === Playback.Roll;
|
|
||||||
const classes = cx([styles.playbackIcon, isPlaying ? styles.active : null]);
|
|
||||||
return <PlaybackIcon state={playback} skipTooltip className={classes} />;
|
|
||||||
}, [playback]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.statusBar}>
|
<div className={styles.statusBar}>
|
||||||
{PlaybackIconComponent}
|
<StatusBarTimers
|
||||||
<div className={styles.timeNow}>
|
projectTitle={projectTitle}
|
||||||
<span className={styles.label}>Time now</span>
|
playback={playback}
|
||||||
<span className={styles.timer}>{timeNow}</span>
|
selectedEventId={selectedEventId}
|
||||||
</div>
|
firstStart={firstStart}
|
||||||
<div className={styles.elapsedTime}>
|
firstId={firstId}
|
||||||
<span className={styles.label}>Elapsed time</span>
|
lastEnd={lastEnd}
|
||||||
<span className={styles.timer}>{elapsedTime}</span>
|
lastId={lastId}
|
||||||
</div>
|
/>
|
||||||
<div className={styles.runningTime}>
|
{data && <StatusBarProgress viewSettings={data} />}
|
||||||
<span className={styles.label}>Running timer</span>
|
|
||||||
<span className={styles.timer}>{runningTime}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span className={styles.title}>{projectTitle}</span>
|
|
||||||
<div className={styles.startTime}>
|
|
||||||
<span className={styles.label}>Scheduled start</span>
|
|
||||||
<span className={styles.timer}>{getTimeStart()}</span>
|
|
||||||
</div>
|
|
||||||
<div className={styles.endTime}>
|
|
||||||
<span className={styles.label}>Scheduled end</span>
|
|
||||||
<span className={styles.timer}>{getTimeEnd()}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
|
import MultiPartProgressBar from '../../../common/components/multi-part-progress-bar/MultiPartProgressBar';
|
||||||
|
import { useTimer } from '../../../common/hooks/useSocket';
|
||||||
|
|
||||||
|
import styles from './StatusBar.module.scss';
|
||||||
|
|
||||||
|
interface StatusBarProgressProps {
|
||||||
|
viewSettings: ViewSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function StatusBarProgress(props: StatusBarProgressProps) {
|
||||||
|
const { viewSettings } = props;
|
||||||
|
|
||||||
|
const timer = useTimer();
|
||||||
|
const totalTime = (timer.duration ?? 0) + (timer.addedTime ?? 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MultiPartProgressBar
|
||||||
|
now={timer.current}
|
||||||
|
complete={totalTime}
|
||||||
|
normalColor={viewSettings.normalColor}
|
||||||
|
warning={viewSettings.warningThreshold}
|
||||||
|
warningColor={viewSettings.warningColor}
|
||||||
|
danger={viewSettings.dangerThreshold}
|
||||||
|
dangerColor={viewSettings.dangerColor}
|
||||||
|
className={styles.progressOverride}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Playback } from 'ontime-types';
|
||||||
|
import { millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
|
import PlaybackIcon from '../../../common/components/playback-icon/PlaybackIcon';
|
||||||
|
import { useTimer } from '../../../common/hooks/useSocket';
|
||||||
|
import { cx } from '../../../common/utils/styleUtils';
|
||||||
|
import { formatTime } from '../../../common/utils/time';
|
||||||
|
|
||||||
|
import styles from './StatusBar.module.scss';
|
||||||
|
|
||||||
|
interface StatusBarTimersProps {
|
||||||
|
projectTitle: string;
|
||||||
|
playback: Playback;
|
||||||
|
selectedEventId: string | null;
|
||||||
|
firstStart?: number;
|
||||||
|
firstId?: string;
|
||||||
|
lastEnd?: number;
|
||||||
|
lastId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function StatusBarTimers(props: StatusBarTimersProps) {
|
||||||
|
const { projectTitle, playback, selectedEventId, firstStart, firstId, lastEnd, lastId } = props;
|
||||||
|
|
||||||
|
const timer = useTimer();
|
||||||
|
|
||||||
|
const getTimeStart = () => {
|
||||||
|
if (firstStart === undefined) {
|
||||||
|
return '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedEventId) {
|
||||||
|
if (firstId === selectedEventId) {
|
||||||
|
return millisToString(timer.expectedFinish);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return millisToString(firstStart);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTimeEnd = () => {
|
||||||
|
if (lastEnd === undefined) {
|
||||||
|
return '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedEventId) {
|
||||||
|
if (lastId === selectedEventId) {
|
||||||
|
return millisToString(timer.expectedFinish);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return millisToString(lastEnd);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PlaybackIconComponent = useMemo(() => {
|
||||||
|
const isPlaying = playback === Playback.Play || playback === Playback.Roll;
|
||||||
|
const classes = cx([styles.playbackIcon, isPlaying ? styles.active : null]);
|
||||||
|
return <PlaybackIcon state={playback} skipTooltip className={classes} />;
|
||||||
|
}, [playback]);
|
||||||
|
|
||||||
|
// use user defined format
|
||||||
|
const timeNow = formatTime(timer.clock, {
|
||||||
|
showSeconds: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const runningTime = millisToString(timer.current);
|
||||||
|
const elapsedTime = millisToString(timer.elapsed);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.timers}>
|
||||||
|
{PlaybackIconComponent}
|
||||||
|
<div className={styles.timeNow}>
|
||||||
|
<span className={styles.label}>Time now</span>
|
||||||
|
<span className={styles.timer}>{timeNow}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.elapsedTime}>
|
||||||
|
<span className={styles.label}>Elapsed time</span>
|
||||||
|
<span className={styles.timer}>{elapsedTime}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.runningTime}>
|
||||||
|
<span className={styles.label}>Running timer</span>
|
||||||
|
<span className={styles.timer}>{runningTime}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className={styles.title}>{projectTitle}</span>
|
||||||
|
<div className={styles.startTime}>
|
||||||
|
<span className={styles.label}>Scheduled start</span>
|
||||||
|
<span className={styles.timer}>{getTimeStart()}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.endTime}>
|
||||||
|
<span className={styles.label}>Scheduled end</span>
|
||||||
|
<span className={styles.timer}>{getTimeEnd()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types';
|
import { GetRundownCached, OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types';
|
||||||
import { calculateDuration, getCueCandidate } from 'ontime-utils';
|
import { calculateDuration, getCueCandidate } from 'ontime-utils';
|
||||||
|
|
||||||
import { RUNDOWN_TABLE } from '../../common/api/apiConstants';
|
import { RUNDOWN } from '../../common/api/apiConstants';
|
||||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||||
import { ontimeQueryClient } from '../../common/queryClient';
|
import { ontimeQueryClient } from '../../common/queryClient';
|
||||||
import { useAppMode } from '../../common/stores/appModeStore';
|
import { useAppMode } from '../../common/stores/appModeStore';
|
||||||
@@ -100,7 +100,8 @@ export default function RundownEntry(props: RundownEntryProps) {
|
|||||||
}
|
}
|
||||||
case 'clone': {
|
case 'clone': {
|
||||||
const newEvent = cloneEvent(data as OntimeEvent, data.id);
|
const newEvent = cloneEvent(data as OntimeEvent, data.id);
|
||||||
newEvent.cue = getCueCandidate(ontimeQueryClient.getQueryData(RUNDOWN_TABLE) || [], data.id);
|
const rundown = ontimeQueryClient.getQueryData<GetRundownCached>(RUNDOWN)?.rundown ?? []
|
||||||
|
newEvent.cue = getCueCandidate(rundown, data.id);
|
||||||
addEvent(newEvent);
|
addEvent(newEvent);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { EventItemActions } from '../../RundownEntry';
|
|||||||
interface BlockActionMenuProps {
|
interface BlockActionMenuProps {
|
||||||
enableDelete?: boolean;
|
enableDelete?: boolean;
|
||||||
showClone?: boolean;
|
showClone?: boolean;
|
||||||
actionHandler: (action: EventItemActions, payload?: unknown) => void;
|
actionHandler: (action: EventItemActions, payload?: any) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,43 @@
|
|||||||
/* eslint-disable react/display-name */
|
|
||||||
import { ComponentType, useMemo } from 'react';
|
import { ComponentType, useMemo } from 'react';
|
||||||
import { SupportedEvent } from 'ontime-types';
|
import { TimeManagerType } from 'common/models/TimeManager.type';
|
||||||
|
import { Message, OntimeEvent, ProjectData, Settings, SupportedEvent, TimerMessage, ViewSettings } from 'ontime-types';
|
||||||
import { useStore } from 'zustand';
|
import { useStore } from 'zustand';
|
||||||
|
|
||||||
import useProjectData from '../../common/hooks-query/useProjectData';
|
import useProjectData from '../../common/hooks-query/useProjectData';
|
||||||
import useRundown from '../../common/hooks-query/useRundown';
|
import useRundown from '../../common/hooks-query/useRundown';
|
||||||
|
import useSettings from '../../common/hooks-query/useSettings';
|
||||||
import useViewSettings from '../../common/hooks-query/useViewSettings';
|
import useViewSettings from '../../common/hooks-query/useViewSettings';
|
||||||
import { runtime } from '../../common/stores/runtime';
|
import { runtime } from '../../common/stores/runtime';
|
||||||
import { useViewOptionsStore } from '../../common/stores/viewOptions';
|
import { useViewOptionsStore } from '../../common/stores/viewOptions';
|
||||||
|
|
||||||
const withData = <P extends object>(Component: ComponentType<P>) => {
|
type WithDataProps = {
|
||||||
return (props: Partial<P>) => {
|
isMirrored: boolean;
|
||||||
|
pres: TimerMessage;
|
||||||
|
publ: Message;
|
||||||
|
lower: Message;
|
||||||
|
external: Message;
|
||||||
|
eventNow: OntimeEvent | null;
|
||||||
|
publicEventNow: OntimeEvent | null;
|
||||||
|
eventNext: OntimeEvent | null;
|
||||||
|
publicEventNext: OntimeEvent | null;
|
||||||
|
time: TimeManagerType;
|
||||||
|
events: OntimeEvent[];
|
||||||
|
backstageEvents: OntimeEvent[];
|
||||||
|
selectedId: string | null;
|
||||||
|
publicSelectedId: string | null;
|
||||||
|
nextId: string | null;
|
||||||
|
general: ProjectData;
|
||||||
|
viewSettings: ViewSettings;
|
||||||
|
settings: Settings | undefined;
|
||||||
|
onAir: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getDisplayName(Component: React.ComponentType<any>): string {
|
||||||
|
return Component.displayName || Component.name || 'Component';
|
||||||
|
}
|
||||||
|
|
||||||
|
const withData = <P extends WithDataProps>(Component: ComponentType<P>) => {
|
||||||
|
const WithDataComponent = (props: P) => {
|
||||||
// persisted app state
|
// persisted app state
|
||||||
const isMirrored = useViewOptionsStore((state) => state.mirror);
|
const isMirrored = useViewOptionsStore((state) => state.mirror);
|
||||||
|
|
||||||
@@ -18,6 +45,7 @@ const withData = <P extends object>(Component: ComponentType<P>) => {
|
|||||||
const { data: rundownData } = useRundown();
|
const { data: rundownData } = useRundown();
|
||||||
const { data: project } = useProjectData();
|
const { data: project } = useProjectData();
|
||||||
const { data: viewSettings } = useViewSettings();
|
const { data: viewSettings } = useViewSettings();
|
||||||
|
const { data: settings } = useSettings();
|
||||||
|
|
||||||
const publicEvents = useMemo(() => {
|
const publicEvents = useMemo(() => {
|
||||||
if (Array.isArray(rundownData)) {
|
if (Array.isArray(rundownData)) {
|
||||||
@@ -27,12 +55,12 @@ const withData = <P extends object>(Component: ComponentType<P>) => {
|
|||||||
}, [rundownData]);
|
}, [rundownData]);
|
||||||
|
|
||||||
// websocket data
|
// websocket data
|
||||||
const data = useStore(runtime);
|
|
||||||
const {
|
const {
|
||||||
timer,
|
timer,
|
||||||
publicMessage,
|
publicMessage,
|
||||||
timerMessage,
|
timerMessage,
|
||||||
lowerMessage,
|
lowerMessage,
|
||||||
|
externalMessage,
|
||||||
playback,
|
playback,
|
||||||
onAir,
|
onAir,
|
||||||
eventNext,
|
eventNext,
|
||||||
@@ -40,7 +68,7 @@ const withData = <P extends object>(Component: ComponentType<P>) => {
|
|||||||
publicEventNow,
|
publicEventNow,
|
||||||
eventNow,
|
eventNow,
|
||||||
loaded,
|
loaded,
|
||||||
} = data;
|
} = useStore(runtime);
|
||||||
const publicSelectedId = loaded.selectedPublicEventId;
|
const publicSelectedId = loaded.selectedPublicEventId;
|
||||||
const selectedId = loaded.selectedEventId;
|
const selectedId = loaded.selectedEventId;
|
||||||
const nextId = loaded.nextEventId;
|
const nextId = loaded.nextEventId;
|
||||||
@@ -68,6 +96,7 @@ const withData = <P extends object>(Component: ComponentType<P>) => {
|
|||||||
pres={timerMessage}
|
pres={timerMessage}
|
||||||
publ={publicMessage}
|
publ={publicMessage}
|
||||||
lower={lowerMessage}
|
lower={lowerMessage}
|
||||||
|
external={externalMessage}
|
||||||
eventNow={eventNow}
|
eventNow={eventNow}
|
||||||
publicEventNow={publicEventNow}
|
publicEventNow={publicEventNow}
|
||||||
eventNext={eventNext}
|
eventNext={eventNext}
|
||||||
@@ -78,12 +107,16 @@ const withData = <P extends object>(Component: ComponentType<P>) => {
|
|||||||
selectedId={selectedId}
|
selectedId={selectedId}
|
||||||
publicSelectedId={publicSelectedId}
|
publicSelectedId={publicSelectedId}
|
||||||
viewSettings={viewSettings}
|
viewSettings={viewSettings}
|
||||||
|
settings={settings}
|
||||||
nextId={nextId}
|
nextId={nextId}
|
||||||
general={project}
|
general={project}
|
||||||
onAir={onAir}
|
onAir={onAir}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
WithDataComponent.displayName = `WithData(${getDisplayName(Component)})`;
|
||||||
|
return WithDataComponent;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withData;
|
export default withData;
|
||||||
|
|||||||
@@ -60,6 +60,7 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--secondary-color-override, $viewer-secondary-color);
|
color: var(--secondary-color-override, $viewer-secondary-color);
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
|
line-height: 0.95em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import QRCode from 'react-qr-code';
|
import QRCode from 'react-qr-code';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { Message, OntimeEvent, ProjectData, SupportedEvent, ViewSettings } from 'ontime-types';
|
import { Message, OntimeEvent, ProjectData, Settings, SupportedEvent, ViewSettings } from 'ontime-types';
|
||||||
import { formatDisplay } from 'ontime-utils';
|
import { formatDisplay } from 'ontime-utils';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||||
@@ -11,13 +11,14 @@ import Schedule from '../../../common/components/schedule/Schedule';
|
|||||||
import { ScheduleProvider } from '../../../common/components/schedule/ScheduleContext';
|
import { ScheduleProvider } from '../../../common/components/schedule/ScheduleContext';
|
||||||
import ScheduleNav from '../../../common/components/schedule/ScheduleNav';
|
import ScheduleNav from '../../../common/components/schedule/ScheduleNav';
|
||||||
import TitleCard from '../../../common/components/title-card/TitleCard';
|
import TitleCard from '../../../common/components/title-card/TitleCard';
|
||||||
import { BACKSTAGE_OPTIONS } from '../../../common/components/view-params-editor/constants';
|
import { getBackstageOptions } from '../../../common/components/view-params-editor/constants';
|
||||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||||
import { formatTime } from '../../../common/utils/time';
|
import { formatTime } from '../../../common/utils/time';
|
||||||
import { useTranslation } from '../../../translation/TranslationProvider';
|
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||||
import { titleVariants } from '../common/animation';
|
import { titleVariants } from '../common/animation';
|
||||||
|
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||||
|
|
||||||
import './Backstage.scss';
|
import './Backstage.scss';
|
||||||
|
|
||||||
@@ -36,10 +37,12 @@ interface BackstageProps {
|
|||||||
selectedId: string | null;
|
selectedId: string | null;
|
||||||
general: ProjectData;
|
general: ProjectData;
|
||||||
viewSettings: ViewSettings;
|
viewSettings: ViewSettings;
|
||||||
|
settings: Settings | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Backstage(props: BackstageProps) {
|
export default function Backstage(props: BackstageProps) {
|
||||||
const { isMirrored, publ, eventNow, eventNext, time, backstageEvents, selectedId, general, viewSettings } = props;
|
const { isMirrored, publ, eventNow, eventNext, time, backstageEvents, selectedId, general, viewSettings, settings } =
|
||||||
|
props;
|
||||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||||
const { getLocalizedString } = useTranslation();
|
const { getLocalizedString } = useTranslation();
|
||||||
const [blinkClass, setBlinkClass] = useState(false);
|
const [blinkClass, setBlinkClass] = useState(false);
|
||||||
@@ -88,16 +91,17 @@ export default function Backstage(props: BackstageProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const totalTime = (time.duration ?? 0) + (time.addedTime ?? 0);
|
const totalTime = (time.duration ?? 0) + (time.addedTime ?? 0);
|
||||||
|
const backstageOptions = getBackstageOptions(settings?.timeFormat ?? '24');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`backstage ${isMirrored ? 'mirror' : ''}`} data-testid='backstage-view'>
|
<div className={`backstage ${isMirrored ? 'mirror' : ''}`} data-testid='backstage-view'>
|
||||||
<NavigationMenu />
|
<NavigationMenu />
|
||||||
<ViewParamsEditor paramFields={BACKSTAGE_OPTIONS} />
|
<ViewParamsEditor paramFields={backstageOptions} />
|
||||||
<div className='project-header'>
|
<div className='project-header'>
|
||||||
{general.title}
|
{general.title}
|
||||||
<div className='clock-container'>
|
<div className='clock-container'>
|
||||||
<div className='label'>{getLocalizedString('common.time_now')}</div>
|
<div className='label'>{getLocalizedString('common.time_now')}</div>
|
||||||
<div className='time'>{clock}</div>
|
<SuperscriptTime time={clock} className='time' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -128,12 +132,16 @@ export default function Backstage(props: BackstageProps) {
|
|||||||
<div className='timer-group'>
|
<div className='timer-group'>
|
||||||
<div className='aux-timers'>
|
<div className='aux-timers'>
|
||||||
<div className='aux-timers__label'>{getLocalizedString('common.started_at')}</div>
|
<div className='aux-timers__label'>{getLocalizedString('common.started_at')}</div>
|
||||||
<div className='aux-timers__value'>{startedAt}</div>
|
<SuperscriptTime time={startedAt} className='aux-timers__value' />
|
||||||
</div>
|
</div>
|
||||||
<div className='timer-gap' />
|
<div className='timer-gap' />
|
||||||
<div className='aux-timers'>
|
<div className='aux-timers'>
|
||||||
<div className='aux-timers__label'>{getLocalizedString('common.expected_finish')}</div>
|
<div className='aux-timers__label'>{getLocalizedString('common.expected_finish')}</div>
|
||||||
<div className='aux-timers__value'>{expectedFinish}</div>
|
{isNegative ? (
|
||||||
|
<div className='aux-timers__value'>{expectedFinish}</div>
|
||||||
|
) : (
|
||||||
|
<SuperscriptTime time={expectedFinish} className='aux-timers__value' />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='timer-gap' />
|
<div className='timer-gap' />
|
||||||
<div className='aux-timers'>
|
<div className='aux-timers'>
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { ViewSettings } from 'ontime-types';
|
import { Settings, ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
import { CLOCK_OPTIONS } from '../../../common/components/view-params-editor/constants';
|
import { getClockOptions } from '../../../common/components/view-params-editor/constants';
|
||||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||||
import { OverridableOptions } from '../../../common/models/View.types';
|
import { OverridableOptions } from '../../../common/models/View.types';
|
||||||
import { formatTime } from '../../../common/utils/time';
|
import { formatTime } from '../../../common/utils/time';
|
||||||
|
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||||
|
|
||||||
import './Clock.scss';
|
import './Clock.scss';
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ interface ClockProps {
|
|||||||
isMirrored: boolean;
|
isMirrored: boolean;
|
||||||
time: TimeManagerType;
|
time: TimeManagerType;
|
||||||
viewSettings: ViewSettings;
|
viewSettings: ViewSettings;
|
||||||
|
settings: Settings | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatOptions = {
|
const formatOptions = {
|
||||||
@@ -25,7 +27,7 @@ const formatOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function Clock(props: ClockProps) {
|
export default function Clock(props: ClockProps) {
|
||||||
const { isMirrored, time, viewSettings } = props;
|
const { isMirrored, time, viewSettings, settings } = props;
|
||||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
@@ -123,6 +125,8 @@ export default function Clock(props: ClockProps) {
|
|||||||
const clock = formatTime(time.clock, formatOptions);
|
const clock = formatTime(time.clock, formatOptions);
|
||||||
const clean = clock.replace('/:/g', '');
|
const clean = clock.replace('/:/g', '');
|
||||||
|
|
||||||
|
const clockOptions = getClockOptions(settings?.timeFormat ?? '24');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`clock-view ${isMirrored ? 'mirror' : ''}`}
|
className={`clock-view ${isMirrored ? 'mirror' : ''}`}
|
||||||
@@ -134,8 +138,9 @@ export default function Clock(props: ClockProps) {
|
|||||||
data-testid='clock-view'
|
data-testid='clock-view'
|
||||||
>
|
>
|
||||||
<NavigationMenu />
|
<NavigationMenu />
|
||||||
<ViewParamsEditor paramFields={CLOCK_OPTIONS} />
|
<ViewParamsEditor paramFields={clockOptions} />
|
||||||
<div
|
<SuperscriptTime
|
||||||
|
time={clock}
|
||||||
className='clock'
|
className='clock'
|
||||||
style={{
|
style={{
|
||||||
color: userOptions.textColour,
|
color: userOptions.textColour,
|
||||||
@@ -145,9 +150,7 @@ export default function Clock(props: ClockProps) {
|
|||||||
left: userOptions.left,
|
left: userOptions.left,
|
||||||
backgroundColor: userOptions.textBackground,
|
backgroundColor: userOptions.textBackground,
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
{clock}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
sup.period {
|
||||||
|
top: -1em;
|
||||||
|
font-size: 0.4em;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { CSSProperties } from 'react';
|
||||||
|
|
||||||
|
import './SuperscriptTime.scss';
|
||||||
|
|
||||||
|
interface SuperscriptTimeProps {
|
||||||
|
time: string;
|
||||||
|
className?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SuperscriptTime(props: SuperscriptTimeProps) {
|
||||||
|
const { time, className, style } = props;
|
||||||
|
|
||||||
|
// we assume anything after space is a period tag
|
||||||
|
const [timeString, period] = time.split(' ');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className} style={style}>
|
||||||
|
{timeString}
|
||||||
|
{period && <sup className='period'>{period}</sup>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -68,6 +68,7 @@
|
|||||||
font-size: clamp(32px, 3.5vw, 50px);
|
font-size: clamp(32px, 3.5vw, 50px);
|
||||||
color: var(--secondary-color-override, $viewer-secondary-color);
|
color: var(--secondary-color-override, $viewer-secondary-color);
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
|
line-height: 0.95em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,6 +136,7 @@
|
|||||||
font-size: clamp(32px, 3.5vw, 50px);
|
font-size: clamp(32px, 3.5vw, 50px);
|
||||||
color: var(--secondary-color-override, $viewer-secondary-color);
|
color: var(--secondary-color-override, $viewer-secondary-color);
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
|
line-height: 0.95em;
|
||||||
|
|
||||||
&--delayed {
|
&--delayed {
|
||||||
color: $delay-color;
|
color: $delay-color;
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent, ViewSettings } from 'ontime-types';
|
import { OntimeEvent, OntimeRundownEntry, Playback, Settings, SupportedEvent, ViewSettings } from 'ontime-types';
|
||||||
import { formatDisplay } from 'ontime-utils';
|
import { formatDisplay } from 'ontime-utils';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
import { TIME_FORMAT_OPTION } from '../../../common/components/view-params-editor/constants';
|
import { getTimeOption } from '../../../common/components/view-params-editor/constants';
|
||||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||||
import { formatTime } from '../../../common/utils/time';
|
import { formatTime } from '../../../common/utils/time';
|
||||||
import { useTranslation } from '../../../translation/TranslationProvider';
|
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||||
|
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||||
|
|
||||||
import { fetchTimerData, TimerMessage } from './countdown.helpers';
|
import { fetchTimerData, TimerMessage } from './countdown.helpers';
|
||||||
import CountdownSelect from './CountdownSelect';
|
import CountdownSelect from './CountdownSelect';
|
||||||
@@ -33,10 +34,11 @@ interface CountdownProps {
|
|||||||
time: TimeManagerType;
|
time: TimeManagerType;
|
||||||
selectedId: string | null;
|
selectedId: string | null;
|
||||||
viewSettings: ViewSettings;
|
viewSettings: ViewSettings;
|
||||||
|
settings: Settings | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Countdown(props: CountdownProps) {
|
export default function Countdown(props: CountdownProps) {
|
||||||
const { isMirrored, backstageEvents, time, selectedId, viewSettings } = props;
|
const { isMirrored, backstageEvents, time, selectedId, viewSettings, settings } = props;
|
||||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const { getLocalizedString } = useTranslation();
|
const { getLocalizedString } = useTranslation();
|
||||||
@@ -108,34 +110,39 @@ export default function Countdown(props: CountdownProps) {
|
|||||||
isSelected || runningMessage === TimerMessage.waiting,
|
isSelected || runningMessage === TimerMessage.waiting,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const timeOption = getTimeOption(settings?.timeFormat ?? '24');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`countdown ${isMirrored ? 'mirror' : ''}`} data-testid='countdown-view'>
|
<div className={`countdown ${isMirrored ? 'mirror' : ''}`} data-testid='countdown-view'>
|
||||||
<NavigationMenu />
|
<NavigationMenu />
|
||||||
<ViewParamsEditor paramFields={[TIME_FORMAT_OPTION]} />
|
<ViewParamsEditor paramFields={[timeOption]} />
|
||||||
{follow === null ? (
|
{follow === null ? (
|
||||||
<CountdownSelect events={backstageEvents} />
|
<CountdownSelect events={backstageEvents} />
|
||||||
) : (
|
) : (
|
||||||
<div className='countdown-container' data-testid='countdown-event'>
|
<div className='countdown-container' data-testid='countdown-event'>
|
||||||
<div className='clock-container'>
|
<div className='clock-container'>
|
||||||
<div className='label'>{getLocalizedString('common.time_now')}</div>
|
<div className='label'>{getLocalizedString('common.time_now')}</div>
|
||||||
<div className='time'>{clock}</div>
|
<SuperscriptTime time={clock} className='time' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='status'>{getLocalizedString(`countdown.${runningMessage}`)}</div>
|
{runningMessage !== TimerMessage.unhandled && (
|
||||||
|
<div className='status'>{getLocalizedString(`countdown.${runningMessage}`)}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<span className={`timer ${standby ? 'timer--paused' : ''} ${isRunningFinished ? 'timer--finished' : ''}`}>
|
<SuperscriptTime
|
||||||
{formattedTimer}
|
time={formattedTimer}
|
||||||
</span>
|
className={`timer ${standby ? 'timer--paused' : ''} ${isRunningFinished ? 'timer--finished' : ''}`}
|
||||||
|
/>
|
||||||
<div className='title'>{follow?.title || 'Untitled Event'}</div>
|
<div className='title'>{follow?.title || 'Untitled Event'}</div>
|
||||||
|
|
||||||
<div className='timer-group'>
|
<div className='timer-group'>
|
||||||
<div className='aux-timers'>
|
<div className='aux-timers'>
|
||||||
<div className='aux-timers__label'>{getLocalizedString('common.start_time')}</div>
|
<div className='aux-timers__label'>{getLocalizedString('common.start_time')}</div>
|
||||||
<span className={`aux-timers__value ${delayedTimerStyles}`}>{startTime}</span>
|
<SuperscriptTime time={startTime} className={`aux-timers__value ${delayedTimerStyles}`} />
|
||||||
</div>
|
</div>
|
||||||
<div className='aux-timers'>
|
<div className='aux-timers'>
|
||||||
<div className='aux-timers__label'>{getLocalizedString('common.end_time')}</div>
|
<div className='aux-timers__label'>{getLocalizedString('common.end_time')}</div>
|
||||||
<span className={`aux-timers__value ${delayedTimerStyles}`}>{endTime}</span>
|
<SuperscriptTime time={endTime} className={`aux-timers__value ${delayedTimerStyles}`} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ import { sanitiseTitle } from './countdown.helpers';
|
|||||||
|
|
||||||
import './Countdown.scss';
|
import './Countdown.scss';
|
||||||
|
|
||||||
|
const formatOptions = {
|
||||||
|
format: 'hh:mm a',
|
||||||
|
};
|
||||||
|
|
||||||
interface CountdownSelectProps {
|
interface CountdownSelectProps {
|
||||||
events: OntimeRundownEntry[];
|
events: OntimeRundownEntry[];
|
||||||
}
|
}
|
||||||
@@ -31,8 +35,8 @@ export default function CountdownSelect(props: CountdownSelectProps) {
|
|||||||
filteredEvents.map((event: OntimeEvent, counter: number) => {
|
filteredEvents.map((event: OntimeEvent, counter: number) => {
|
||||||
const index = counter + 1;
|
const index = counter + 1;
|
||||||
const title = sanitiseTitle(event.title);
|
const title = sanitiseTitle(event.title);
|
||||||
const start = formatTime(event.timeStart, { format: 'hh:mm' });
|
const start = formatTime(event.timeStart, formatOptions);
|
||||||
const end = formatTime(event.timeEnd, { format: 'hh:mm' });
|
const end = formatTime(event.timeEnd, formatOptions);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={event.id}>
|
<li key={event.id}>
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
font-size: clamp(32px, 3.5vw, 50px);
|
font-size: clamp(32px, 3.5vw, 50px);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
|
line-height: 0.95em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import QRCode from 'react-qr-code';
|
import QRCode from 'react-qr-code';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { Message, OntimeEvent, ProjectData, ViewSettings } from 'ontime-types';
|
import { Message, OntimeEvent, ProjectData, Settings, ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
@@ -9,13 +9,14 @@ import Schedule from '../../../common/components/schedule/Schedule';
|
|||||||
import { ScheduleProvider } from '../../../common/components/schedule/ScheduleContext';
|
import { ScheduleProvider } from '../../../common/components/schedule/ScheduleContext';
|
||||||
import ScheduleNav from '../../../common/components/schedule/ScheduleNav';
|
import ScheduleNav from '../../../common/components/schedule/ScheduleNav';
|
||||||
import TitleCard from '../../../common/components/title-card/TitleCard';
|
import TitleCard from '../../../common/components/title-card/TitleCard';
|
||||||
import { PUBLIC_OPTIONS } from '../../../common/components/view-params-editor/constants';
|
import { getPublicOptions } from '../../../common/components/view-params-editor/constants';
|
||||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||||
import { formatTime } from '../../../common/utils/time';
|
import { formatTime } from '../../../common/utils/time';
|
||||||
import { useTranslation } from '../../../translation/TranslationProvider';
|
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||||
import { titleVariants } from '../common/animation';
|
import { titleVariants } from '../common/animation';
|
||||||
|
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||||
|
|
||||||
import './Public.scss';
|
import './Public.scss';
|
||||||
|
|
||||||
@@ -34,11 +35,22 @@ interface BackstageProps {
|
|||||||
publicSelectedId: string | null;
|
publicSelectedId: string | null;
|
||||||
general: ProjectData;
|
general: ProjectData;
|
||||||
viewSettings: ViewSettings;
|
viewSettings: ViewSettings;
|
||||||
|
settings: Settings | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Public(props: BackstageProps) {
|
export default function Public(props: BackstageProps) {
|
||||||
const { isMirrored, publ, publicEventNow, publicEventNext, time, events, publicSelectedId, general, viewSettings } =
|
const {
|
||||||
props;
|
isMirrored,
|
||||||
|
publ,
|
||||||
|
publicEventNow,
|
||||||
|
publicEventNext,
|
||||||
|
time,
|
||||||
|
events,
|
||||||
|
publicSelectedId,
|
||||||
|
general,
|
||||||
|
viewSettings,
|
||||||
|
settings,
|
||||||
|
} = props;
|
||||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||||
const { getLocalizedString } = useTranslation();
|
const { getLocalizedString } = useTranslation();
|
||||||
|
|
||||||
@@ -55,15 +67,17 @@ export default function Public(props: BackstageProps) {
|
|||||||
const clock = formatTime(time.clock, formatOptions);
|
const clock = formatTime(time.clock, formatOptions);
|
||||||
const qrSize = Math.max(window.innerWidth / 15, 128);
|
const qrSize = Math.max(window.innerWidth / 15, 128);
|
||||||
|
|
||||||
|
const publicOptions = getPublicOptions(settings?.timeFormat ?? '24');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`public-screen ${isMirrored ? 'mirror' : ''}`} data-testid='public-view'>
|
<div className={`public-screen ${isMirrored ? 'mirror' : ''}`} data-testid='public-view'>
|
||||||
<NavigationMenu />
|
<NavigationMenu />
|
||||||
<ViewParamsEditor paramFields={PUBLIC_OPTIONS} />
|
<ViewParamsEditor paramFields={publicOptions} />
|
||||||
<div className='project-header'>
|
<div className='project-header'>
|
||||||
{general.title}
|
{general.title}
|
||||||
<div className='clock-container'>
|
<div className='clock-container'>
|
||||||
<div className='label'>{getLocalizedString('common.time_now')}</div>
|
<div className='label'>{getLocalizedString('common.time_now')}</div>
|
||||||
<div className='time'>{clock}</div>
|
<SuperscriptTime time={clock} className='time' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ $half-hours: min(1.5vh, 10px);
|
|||||||
$size-min: min(2.5vh, 18px);
|
$size-min: min(2.5vh, 18px);
|
||||||
$half-min: min(1.25vh, 9px);
|
$half-min: min(1.25vh, 9px);
|
||||||
$red-active: #c53030;
|
$red-active: #c53030;
|
||||||
$red-idle: #300000;
|
$red-idle: #000000;
|
||||||
$cyan-active: #0ff;
|
$cyan-active: #0ff;
|
||||||
$cyan-idle: #0aa;
|
$cyan-idle: #0aa;
|
||||||
|
|
||||||
@@ -56,11 +56,10 @@ $cyan-idle: #0aa;
|
|||||||
.hours {
|
.hours {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background: $red-idle;
|
background: var(--studio-idle, $red-idle);
|
||||||
|
|
||||||
&--active {
|
&--active {
|
||||||
background: $red-active;
|
background: var(--studio-active, $red-active);
|
||||||
box-shadow: 0 0 10px 2px rgba(255, 0, 0, 0.25);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +79,7 @@ $cyan-idle: #0aa;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.studio-timer {
|
.studio-timer {
|
||||||
color: $red-active;
|
color: var(--studio-active, $red-active);
|
||||||
font-size: calc(#{$clock-size} / 3);
|
font-size: calc(#{$clock-size} / 3);
|
||||||
margin-top: calc(50% - calc(#{$clock-size} / 7));
|
margin-top: calc(50% - calc(#{$clock-size} / 7));
|
||||||
line-height: 0.8em;
|
line-height: 0.8em;
|
||||||
@@ -88,9 +87,8 @@ $cyan-idle: #0aa;
|
|||||||
&--with-seconds {
|
&--with-seconds {
|
||||||
font-size: calc(#{$clock-size} / 4.6);
|
font-size: calc(#{$clock-size} / 4.6);
|
||||||
margin-top: calc(50% - calc(#{$clock-size} / 11));
|
margin-top: calc(50% - calc(#{$clock-size} / 11));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.next-title:after,
|
.next-title:after,
|
||||||
@@ -100,14 +98,15 @@ $cyan-idle: #0aa;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.next-title {
|
.next-title {
|
||||||
color: $cyan-idle;
|
color: var(--studio-idle-label, $cyan-idle);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.next-countdown {
|
.next-countdown {
|
||||||
|
color: var(--studio-active-label, $cyan-active);
|
||||||
|
|
||||||
font-size: 10vh;
|
font-size: 10vh;
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
color: $cyan-active;
|
|
||||||
|
|
||||||
&--overtime {
|
&--overtime {
|
||||||
color: darken($red-active, 10%);
|
color: darken($red-active, 10%);
|
||||||
@@ -131,16 +130,16 @@ $cyan-idle: #0aa;
|
|||||||
padding-bottom: 2vh;
|
padding-bottom: 2vh;
|
||||||
font-size: 15vh;
|
font-size: 15vh;
|
||||||
line-height: 0.9em;
|
line-height: 0.9em;
|
||||||
color: $red-active;
|
color: var(--studio-active, $red-active);
|
||||||
|
|
||||||
&--idle {
|
&--idle {
|
||||||
color: $red-idle;
|
color: var(--studio-idle, $red-active);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.schedule {
|
.schedule {
|
||||||
ul {
|
ul {
|
||||||
color: $cyan-idle;
|
color: var(--studio-idle-label, $cyan-idle);
|
||||||
font-size: 3.75vh;
|
font-size: 3.75vh;
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
@@ -154,18 +153,18 @@ $cyan-idle: #0aa;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.now {
|
.now {
|
||||||
color: $cyan-active;
|
color: var(--studio-active-label, $cyan-active);
|
||||||
}
|
}
|
||||||
|
|
||||||
.next {
|
.next {
|
||||||
color: $red-active;
|
color: var(--studio-active, $red-active);
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-colour {
|
.user-colour {
|
||||||
width: 0.35em;
|
width: 0.35em;
|
||||||
height: 0.35em;
|
height: 0.35em;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
background-color: $red-idle;
|
background-color: var(--studio-idle, $red-idle);
|
||||||
margin-right: 0.35em;
|
margin-right: 0.35em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import type { OntimeEvent, OntimeRundown, ViewSettings } from 'ontime-types';
|
import type { OntimeEvent, OntimeRundown, Settings, ViewSettings } from 'ontime-types';
|
||||||
import { SupportedEvent } from 'ontime-types';
|
import { SupportedEvent } from 'ontime-types';
|
||||||
import { formatDisplay } from 'ontime-utils';
|
import { formatDisplay } from 'ontime-utils';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
import { STUDIO_CLOCK_OPTIONS } from '../../../common/components/view-params-editor/constants';
|
import { getStudioClockOptions } from '../../../common/components/view-params-editor/constants';
|
||||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
import useFitText from '../../../common/hooks/useFitText';
|
import useFitText from '../../../common/hooks/useFitText';
|
||||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
@@ -32,10 +32,11 @@ interface StudioClockProps {
|
|||||||
nextId: string | null;
|
nextId: string | null;
|
||||||
onAir: boolean;
|
onAir: boolean;
|
||||||
viewSettings: ViewSettings;
|
viewSettings: ViewSettings;
|
||||||
|
settings: Settings | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function StudioClock(props: StudioClockProps) {
|
export default function StudioClock(props: StudioClockProps) {
|
||||||
const { isMirrored, eventNext, time, backstageEvents, selectedId, nextId, onAir, viewSettings } = props;
|
const { isMirrored, eventNext, time, backstageEvents, selectedId, nextId, onAir, viewSettings, settings } = props;
|
||||||
|
|
||||||
// deferring rendering seems to affect styling (font and useFitText)
|
// deferring rendering seems to affect styling (font and useFitText)
|
||||||
useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||||
@@ -75,10 +76,12 @@ export default function StudioClock(props: StudioClockProps) {
|
|||||||
const secondsNow = secondsInMillis(time.clock);
|
const secondsNow = secondsInMillis(time.clock);
|
||||||
const isNegative = (time.current ?? 0) < 0;
|
const isNegative = (time.current ?? 0) < 0;
|
||||||
|
|
||||||
|
const studioClockOptions = getStudioClockOptions(settings?.timeFormat ?? '24');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`studio-clock ${isMirrored ? 'mirror' : ''}`} data-testid='studio-view'>
|
<div className={`studio-clock ${isMirrored ? 'mirror' : ''}`} data-testid='studio-view'>
|
||||||
<NavigationMenu />
|
<NavigationMenu />
|
||||||
<ViewParamsEditor paramFields={STUDIO_CLOCK_OPTIONS} />
|
<ViewParamsEditor paramFields={studioClockOptions} />
|
||||||
<div className='clock-container'>
|
<div className='clock-container'>
|
||||||
<div className={`studio-timer ${showSeconds ? 'studio-timer--with-seconds' : ''}`}>{clock}</div>
|
<div className={`studio-timer ${showSeconds ? 'studio-timer--with-seconds' : ''}`}>{clock}</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
font-size: clamp(32px, 3.5vw, 50px);
|
font-size: clamp(32px, 3.5vw, 50px);
|
||||||
color: var(--secondary-color-override, $viewer-secondary-color);
|
color: var(--secondary-color-override, $viewer-secondary-color);
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
|
line-height: 0.95em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--hidden {
|
&--hidden {
|
||||||
@@ -78,6 +79,9 @@
|
|||||||
justify-self: center;
|
justify-self: center;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
.end-message {
|
.end-message {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 11.5vw;
|
font-size: 11.5vw;
|
||||||
@@ -89,7 +93,6 @@
|
|||||||
|
|
||||||
.timer {
|
.timer {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
font-size: 20vw;
|
|
||||||
font-family: var(--font-family-override, $viewer-font-family);
|
font-family: var(--font-family-override, $viewer-font-family);
|
||||||
color: var(--timer-color-override, $timer-color);
|
color: var(--timer-color-override, $timer-color);
|
||||||
line-height: 0.9em;
|
line-height: 0.9em;
|
||||||
@@ -97,6 +100,9 @@
|
|||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
||||||
|
transition-property: font-size;
|
||||||
|
transition-duration: $viewer-transition-time;
|
||||||
|
|
||||||
&--paused {
|
&--paused {
|
||||||
opacity: $viewer-opacity-disabled;
|
opacity: $viewer-opacity-disabled;
|
||||||
transition: $viewer-transition-time;
|
transition: $viewer-transition-time;
|
||||||
@@ -108,6 +114,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.external {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--external-color-override, $external-color);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
line-height: 0.9em;
|
||||||
|
padding-bottom: 0.2em;
|
||||||
|
transition-property: opacity, height;
|
||||||
|
transition-duration: $viewer-transition-time;
|
||||||
|
border-top: 1px solid rgba(white, 0.1);
|
||||||
|
|
||||||
|
&--hidden {
|
||||||
|
opacity: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.progress-container {
|
.progress-container {
|
||||||
grid-area: progress;
|
grid-area: progress;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -115,12 +142,11 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition: $viewer-transition-time;
|
transition: $viewer-transition-time;
|
||||||
|
|
||||||
&--paused {
|
&--paused {
|
||||||
opacity: $viewer-opacity-disabled;
|
opacity: $viewer-opacity-disabled;
|
||||||
transition: $viewer-transition-time;
|
transition: $viewer-transition-time;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* =================== OVERLAY ===================*/
|
/* =================== OVERLAY ===================*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { OntimeEvent, Playback, TimerMessage, TimerType, ViewSettings } from 'ontime-types';
|
import { Message, OntimeEvent, Playback, Settings, TimerMessage, TimerType, ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||||
import MultiPartProgressBar from '../../../common/components/multi-part-progress-bar/MultiPartProgressBar';
|
import MultiPartProgressBar from '../../../common/components/multi-part-progress-bar/MultiPartProgressBar';
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
import TitleCard from '../../../common/components/title-card/TitleCard';
|
import TitleCard from '../../../common/components/title-card/TitleCard';
|
||||||
import { TIMER_OPTIONS } from '../../../common/components/view-params-editor/constants';
|
import { getTimerOptions } from '../../../common/components/view-params-editor/constants';
|
||||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||||
import { formatTime } from '../../../common/utils/time';
|
import { formatTime } from '../../../common/utils/time';
|
||||||
|
import { isStringBoolean } from '../../../common/utils/viewUtils';
|
||||||
import { useTranslation } from '../../../translation/TranslationProvider';
|
import { useTranslation } from '../../../translation/TranslationProvider';
|
||||||
|
import SuperscriptTime from '../common/superscript-time/SuperscriptTime';
|
||||||
import { formatTimerDisplay, getTimerByType } from '../common/viewerUtils';
|
import { formatTimerDisplay, getTimerByType } from '../common/viewerUtils';
|
||||||
|
|
||||||
import './Timer.scss';
|
import './Timer.scss';
|
||||||
@@ -40,16 +43,19 @@ const titleVariants = {
|
|||||||
interface TimerProps {
|
interface TimerProps {
|
||||||
isMirrored: boolean;
|
isMirrored: boolean;
|
||||||
pres: TimerMessage;
|
pres: TimerMessage;
|
||||||
|
external: Message;
|
||||||
eventNow: OntimeEvent | null;
|
eventNow: OntimeEvent | null;
|
||||||
eventNext: OntimeEvent | null;
|
eventNext: OntimeEvent | null;
|
||||||
time: TimeManagerType;
|
time: TimeManagerType;
|
||||||
viewSettings: ViewSettings;
|
viewSettings: ViewSettings;
|
||||||
|
settings: Settings | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Timer(props: TimerProps) {
|
export default function Timer(props: TimerProps) {
|
||||||
const { isMirrored, pres, eventNow, eventNext, time, viewSettings } = props;
|
const { isMirrored, pres, eventNow, eventNext, time, viewSettings, external, settings } = props;
|
||||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||||
const { getLocalizedString } = useTranslation();
|
const { getLocalizedString } = useTranslation();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = 'ontime - Timer';
|
document.title = 'ontime - Timer';
|
||||||
@@ -60,6 +66,26 @@ export default function Timer(props: TimerProps) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// USER OPTIONS
|
||||||
|
const userOptions = {
|
||||||
|
hideClock: false,
|
||||||
|
hideCards: false,
|
||||||
|
hideProgress: false,
|
||||||
|
hideMessage: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideClock = searchParams.get('hideClock');
|
||||||
|
userOptions.hideClock = isStringBoolean(hideClock);
|
||||||
|
|
||||||
|
const hideCards = searchParams.get('hideCards');
|
||||||
|
userOptions.hideCards = isStringBoolean(hideCards);
|
||||||
|
|
||||||
|
const hideProgress = searchParams.get('hideProgress');
|
||||||
|
userOptions.hideProgress = isStringBoolean(hideProgress);
|
||||||
|
|
||||||
|
const hideMessage = searchParams.get('hideMessage');
|
||||||
|
userOptions.hideMessage = isStringBoolean(hideMessage);
|
||||||
|
|
||||||
const clock = formatTime(time.clock, formatOptions);
|
const clock = formatTime(time.clock, formatOptions);
|
||||||
const showOverlay = pres.text !== '' && pres.visible;
|
const showOverlay = pres.text !== '' && pres.visible;
|
||||||
const isPlaying = time.playback !== Playback.Pause;
|
const isPlaying = time.playback !== Playback.Pause;
|
||||||
@@ -77,6 +103,7 @@ export default function Timer(props: TimerProps) {
|
|||||||
const showBlinking = pres.timerBlink;
|
const showBlinking = pres.timerBlink;
|
||||||
const showBlackout = pres.timerBlackout;
|
const showBlackout = pres.timerBlackout;
|
||||||
const showClock = time.timerType !== TimerType.Clock;
|
const showClock = time.timerType !== TimerType.Clock;
|
||||||
|
const showExternal = external.visible && external.text;
|
||||||
|
|
||||||
const timerColor =
|
const timerColor =
|
||||||
showProgress && showDanger
|
showProgress && showDanger
|
||||||
@@ -93,23 +120,35 @@ export default function Timer(props: TimerProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const baseClasses = `stage-timer ${isMirrored ? 'mirror' : ''} ${showBlackout ? 'blackout' : ''}`;
|
const baseClasses = `stage-timer ${isMirrored ? 'mirror' : ''} ${showBlackout ? 'blackout' : ''}`;
|
||||||
const timerFontSize = 89 / (stageTimerCharacters - 1);
|
let timerFontSize = 89 / (stageTimerCharacters - 1);
|
||||||
|
// we need to shrink the timer if the external is going to be there
|
||||||
|
if (showExternal) {
|
||||||
|
timerFontSize *= 0.8;
|
||||||
|
}
|
||||||
|
const externalFontSize = timerFontSize * 0.4;
|
||||||
|
const timerContainerClasses = `timer-container ${showBlinking ? (showOverlay ? '' : 'blink') : ''}`;
|
||||||
const timerClasses = `timer ${!isPlaying ? 'timer--paused' : ''} ${showFinished ? 'timer--finished' : ''}`;
|
const timerClasses = `timer ${!isPlaying ? 'timer--paused' : ''} ${showFinished ? 'timer--finished' : ''}`;
|
||||||
|
|
||||||
|
const timerOptions = getTimerOptions(settings?.timeFormat ?? '24');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={showFinished ? `${baseClasses} stage-timer--finished` : baseClasses} data-testid='timer-view'>
|
<div className={showFinished ? `${baseClasses} stage-timer--finished` : baseClasses} data-testid='timer-view'>
|
||||||
<NavigationMenu />
|
<NavigationMenu />
|
||||||
<ViewParamsEditor paramFields={TIMER_OPTIONS} />
|
<ViewParamsEditor paramFields={timerOptions} />
|
||||||
<div className={showOverlay ? 'message-overlay message-overlay--active' : 'message-overlay'}>
|
{!userOptions.hideMessage && (
|
||||||
<div className={`message ${showBlinking ? 'blink' : ''}`}>{pres.text}</div>
|
<div className={showOverlay ? 'message-overlay message-overlay--active' : 'message-overlay'}>
|
||||||
</div>
|
<div className={`message ${showBlinking ? 'blink' : ''}`}>{pres.text}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={`clock-container ${showClock ? '' : 'clock-container--hidden'}`}>
|
{!userOptions.hideClock && (
|
||||||
<div className='label'>{getLocalizedString('common.time_now')}</div>
|
<div className={`clock-container ${showClock ? '' : 'clock-container--hidden'}`}>
|
||||||
<div className='clock'>{clock}</div>
|
<div className='label'>{getLocalizedString('common.time_now')}</div>
|
||||||
</div>
|
<SuperscriptTime time={clock} className='clock' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={`timer-container ${showBlinking ? (showOverlay ? '' : 'blink') : ''}`}>
|
<div className={timerContainerClasses}>
|
||||||
{showEndMessage ? (
|
{showEndMessage ? (
|
||||||
<div className='end-message'>{viewSettings.endMessage}</div>
|
<div className='end-message'>{viewSettings.endMessage}</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -123,54 +162,71 @@ export default function Timer(props: TimerProps) {
|
|||||||
{display}
|
{display}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div
|
||||||
|
className={`external${showExternal ? '' : ' external--hidden'}`}
|
||||||
|
style={{ fontSize: `${externalFontSize}vw` }}
|
||||||
|
>
|
||||||
|
{external.text}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MultiPartProgressBar
|
{!userOptions.hideProgress && (
|
||||||
className={isPlaying ? 'progress-container' : 'progress-container progress-container--paused'}
|
<MultiPartProgressBar
|
||||||
now={time.current || 0}
|
className={isPlaying ? 'progress-container' : 'progress-container progress-container--paused'}
|
||||||
complete={totalTime}
|
now={time.current}
|
||||||
normalColor={viewSettings.normalColor}
|
complete={totalTime}
|
||||||
warning={viewSettings.warningThreshold}
|
normalColor={viewSettings.normalColor}
|
||||||
warningColor={viewSettings.warningColor}
|
warning={viewSettings.warningThreshold}
|
||||||
danger={viewSettings.dangerThreshold}
|
warningColor={viewSettings.warningColor}
|
||||||
dangerColor={viewSettings.dangerColor}
|
danger={viewSettings.dangerThreshold}
|
||||||
hidden={!showProgress}
|
dangerColor={viewSettings.dangerColor}
|
||||||
/>
|
hidden={!showProgress}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<AnimatePresence>
|
{!userOptions.hideCards && (
|
||||||
{eventNow && !finished && (
|
<>
|
||||||
<motion.div
|
<AnimatePresence>
|
||||||
className='event now'
|
{eventNow && !finished && (
|
||||||
key='now'
|
<motion.div
|
||||||
variants={titleVariants}
|
className='event now'
|
||||||
initial='hidden'
|
key='now'
|
||||||
animate='visible'
|
variants={titleVariants}
|
||||||
exit='exit'
|
initial='hidden'
|
||||||
>
|
animate='visible'
|
||||||
<TitleCard label='now' title={eventNow.title} subtitle={eventNow.subtitle} presenter={eventNow.presenter} />
|
exit='exit'
|
||||||
</motion.div>
|
>
|
||||||
)}
|
<TitleCard
|
||||||
</AnimatePresence>
|
label='now'
|
||||||
|
title={eventNow.title}
|
||||||
|
subtitle={eventNow.subtitle}
|
||||||
|
presenter={eventNow.presenter}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{eventNext && (
|
{eventNext && (
|
||||||
<motion.div
|
<motion.div
|
||||||
className='event next'
|
className='event next'
|
||||||
key='next'
|
key='next'
|
||||||
variants={titleVariants}
|
variants={titleVariants}
|
||||||
initial='hidden'
|
initial='hidden'
|
||||||
animate='visible'
|
animate='visible'
|
||||||
exit='exit'
|
exit='exit'
|
||||||
>
|
>
|
||||||
<TitleCard
|
<TitleCard
|
||||||
label='next'
|
label='next'
|
||||||
title={eventNext.title}
|
title={eventNext.title}
|
||||||
subtitle={eventNext.subtitle}
|
subtitle={eventNext.subtitle}
|
||||||
presenter={eventNext.presenter}
|
presenter={eventNext.presenter}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,23 +2,6 @@
|
|||||||
|
|
||||||
//////////////////////////////////// general app elements
|
//////////////////////////////////// general app elements
|
||||||
|
|
||||||
@mixin main-container {
|
|
||||||
background-color: $bg-container-l1;
|
|
||||||
border: 1px solid $bg-container-l1;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin second-container {
|
|
||||||
background-color: $bg-container-l2;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin third-container {
|
|
||||||
background-color: $bg-container-l3;
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin action-link {
|
@mixin action-link {
|
||||||
color: $action-text-color;
|
color: $action-text-color;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
// General
|
// General
|
||||||
$viewer-transition-time: 0.5s;
|
$viewer-transition-time: 0.5s;
|
||||||
|
|
||||||
|
|
||||||
// Text
|
// Text
|
||||||
$viewer-font-family: "Open Sans", "Segoe UI", sans-serif; // --font-family-override
|
$viewer-font-family: "Open Sans", "Segoe UI", sans-serif; // --font-family-override
|
||||||
$viewer-opacity-disabled: 0.6;
|
$viewer-opacity-disabled: 0.6;
|
||||||
@@ -28,3 +27,4 @@ $timer-color: rgba(white, 80%); // --timer-color-override
|
|||||||
$timer-finished-color: $playback-negative;
|
$timer-finished-color: $playback-negative;
|
||||||
$timer-bold-font-family: "Arial Black", sans-serif; // --card-background-color-override
|
$timer-bold-font-family: "Arial Black", sans-serif; // --card-background-color-override
|
||||||
|
|
||||||
|
$external-color: rgba(white, 70%); // --external-color-override
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ const commonStyles = {
|
|||||||
border: '1px solid #578AF4', // $blue-500
|
border: '1px solid #578AF4', // $blue-500
|
||||||
},
|
},
|
||||||
_placeholder: { color: '#9d9d9d' }, // $gray-500
|
_placeholder: { color: '#9d9d9d' }, // $gray-500
|
||||||
|
_disabled: {
|
||||||
|
_hover: {
|
||||||
|
backgroundColor: '#262626', // $gray-1200
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ontimeInputFilled = {
|
export const ontimeInputFilled = {
|
||||||
@@ -30,6 +35,11 @@ export const ontimeInputFilledOnLight = {
|
|||||||
_focus: {
|
_focus: {
|
||||||
border: '2px solid #578AF4', // $blue-500
|
border: '2px solid #578AF4', // $blue-500
|
||||||
},
|
},
|
||||||
|
_disabled: {
|
||||||
|
_hover: {
|
||||||
|
backgroundColor: 'white',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -58,4 +68,9 @@ export const ontimeTextAreaFilledOnLight = {
|
|||||||
border: '2px solid #578AF4', // $blue-500
|
border: '2px solid #578AF4', // $blue-500
|
||||||
},
|
},
|
||||||
_placeholder: { color: '#9d9d9d' }, // $gray-500
|
_placeholder: { color: '#9d9d9d' }, // $gray-500
|
||||||
|
_disabled: {
|
||||||
|
_hover: {
|
||||||
|
backgroundColor: 'white',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ontime",
|
"name": "ontime",
|
||||||
"version": "2.16.2",
|
"version": "2.24.8",
|
||||||
"author": "Carlos Valente",
|
"author": "Carlos Valente",
|
||||||
"description": "Time keeping for live events",
|
"description": "Time keeping for live events",
|
||||||
"repository": "https://github.com/cpvalente/ontime",
|
"repository": "https://github.com/cpvalente/ontime",
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "",
|
"postinstall": "",
|
||||||
"lint": "eslint . --quiet",
|
"lint": "eslint . --quiet",
|
||||||
|
"lint-staged": "eslint",
|
||||||
"dev:electron": "cross-env NODE_ENV=development electron .",
|
"dev:electron": "cross-env NODE_ENV=development electron .",
|
||||||
"dist-win": "electron-builder --publish=never --x64 --win",
|
"dist-win": "electron-builder --publish=never --x64 --win",
|
||||||
"dist-mac": "electron-builder --publish=never --mac",
|
"dist-mac": "electron-builder --publish=never --mac",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "ontime-server",
|
"name": "ontime-server",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"version": "2.16.2",
|
"version": "2.24.8",
|
||||||
"exports": "./src/index.js",
|
"exports": "./src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"body-parser": "^1.20.0",
|
"body-parser": "^1.20.0",
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
"express-static-gzip": "^2.1.7",
|
"express-static-gzip": "^2.1.7",
|
||||||
"express-validator": "^6.14.2",
|
"express-validator": "^6.14.2",
|
||||||
|
"got": "^14.0.0",
|
||||||
"lowdb": "^5.0.5",
|
"lowdb": "^5.0.5",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"node-osc": "^9.0.2",
|
"node-osc": "^9.0.2",
|
||||||
@@ -49,10 +50,12 @@
|
|||||||
"dev:test": "cross-env IS_TEST=true nodemon --exec \"ts-node-esm\" ./src/index.ts",
|
"dev:test": "cross-env IS_TEST=true nodemon --exec \"ts-node-esm\" ./src/index.ts",
|
||||||
"prebuild": "pnpm setdb",
|
"prebuild": "pnpm setdb",
|
||||||
"build": "pnpm prebuild && esbuild src/app.ts --log-level=error --platform=node --format=cjs --bundle --minify --outfile=dist/index.cjs",
|
"build": "pnpm prebuild && esbuild src/app.ts --log-level=error --platform=node --format=cjs --bundle --minify --outfile=dist/index.cjs",
|
||||||
|
"build:electron": "pnpm prebuild && esbuild src/app.ts --log-level=error --platform=node --format=cjs --bundle --minify --outfile=dist/index.cjs",
|
||||||
"build:local": "pnpm prebuild && esbuild src/index.ts --log-level=error --platform=node --format=cjs --bundle --minify --outfile=dist/index.cjs",
|
"build:local": "pnpm prebuild && esbuild src/index.ts --log-level=error --platform=node --format=cjs --bundle --minify --outfile=dist/index.cjs",
|
||||||
"build:docker": "pnpm prebuild && esbuild src/index.ts --log-level=error --platform=node --format=cjs --minify --bundle --outfile=dist/docker.cjs",
|
"build:docker": "pnpm prebuild && esbuild src/index.ts --log-level=error --platform=node --format=cjs --minify --bundle --outfile=dist/docker.cjs",
|
||||||
"build:debug": "pnpm prebuild && esbuild src/app.ts --platform=node --format=cjs --bundle --outfile=dist/index.cjs",
|
"build:debug": "pnpm prebuild && esbuild src/app.ts --platform=node --format=cjs --bundle --outfile=dist/index.cjs",
|
||||||
"lint": "eslint . --quiet",
|
"lint": "eslint . --quiet",
|
||||||
|
"lint-staged": "eslint",
|
||||||
"test": "cross-env IS_TEST=true vitest",
|
"test": "cross-env IS_TEST=true vitest",
|
||||||
"test:pipeline": "cross-env IS_TEST=true vitest run",
|
"test:pipeline": "cross-env IS_TEST=true vitest run",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { LogOrigin, OSCSettings } from 'ontime-types';
|
|||||||
import { Server } from 'node-osc';
|
import { Server } from 'node-osc';
|
||||||
|
|
||||||
import { IAdapter } from './IAdapter.js';
|
import { IAdapter } from './IAdapter.js';
|
||||||
import { dispatchFromAdapter } from '../controllers/integrationController.js';
|
import { dispatchFromAdapter, type ChangeOptions } from '../controllers/integrationController.js';
|
||||||
import { logger } from '../classes/Logger.js';
|
import { logger } from '../classes/Logger.js';
|
||||||
|
|
||||||
export class OscServer implements IAdapter {
|
export class OscServer implements IAdapter {
|
||||||
@@ -36,12 +36,36 @@ export class OscServer implements IAdapter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let transformedPayload: unknown = args;
|
||||||
|
// we need to transform the params for the change endpoint
|
||||||
|
// OSC: ontime/change/{eventID}/{propertyName} value
|
||||||
|
if (path === 'change') {
|
||||||
|
if (params.length < 2) {
|
||||||
|
logger.error(LogOrigin.Rx, 'OSC IN: No params provided for change');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args === undefined) {
|
||||||
|
logger.error(LogOrigin.Rx, 'OSC IN: No valid payload provided for change');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventId = params[0];
|
||||||
|
const property = params[1];
|
||||||
|
const value: string | number | boolean = args as string | number | boolean;
|
||||||
|
|
||||||
|
transformedPayload = {
|
||||||
|
eventId,
|
||||||
|
property,
|
||||||
|
value,
|
||||||
|
} satisfies ChangeOptions;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const reply = dispatchFromAdapter(
|
const reply = dispatchFromAdapter(
|
||||||
path,
|
path,
|
||||||
{
|
{
|
||||||
payload: args,
|
payload: transformedPayload,
|
||||||
params,
|
|
||||||
},
|
},
|
||||||
'osc',
|
'osc',
|
||||||
);
|
);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user