mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-19 14:14:17 +00:00
v2 alpha5 (#315)
* fix: fetch in offline environments (#295) * add arm platforms to docker build --------- Co-authored-by: Fabian Posenau <fabian@fphome.de> * fix: docker build (#298) Co-authored-by: Fabian Posenau <fabian@fphome.de> * Timer: fix too many renders error when using ?progress (#305) * ux: remove duplicate showing of selected event * several small fixes and UX improvements --------- Co-authored-by: Fabian Posenau <fabian.p99@gmx.de> Co-authored-by: Fabian Posenau <fabian@fphome.de> Co-authored-by: Marks Polakovs <github@markspolakovs.me>
This commit is contained in:
@@ -141,3 +141,4 @@ jobs:
|
|||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:${{ env.RELEASE_VERSION }} , ${{ secrets.DOCKERHUB_USERNAME }}/ontime:latest
|
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:${{ env.RELEASE_VERSION }} , ${{ secrets.DOCKERHUB_USERNAME }}/ontime:latest
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
"csv-stringify": "^6.2.3",
|
"csv-stringify": "^6.2.3",
|
||||||
"deepmerge": "^4.3.0",
|
"deepmerge": "^4.3.0",
|
||||||
"framer-motion": "^8.0.2",
|
"framer-motion": "^8.0.2",
|
||||||
"jotai": "^1.10.0",
|
|
||||||
"luxon": "^3.3.0",
|
"luxon": "^3.3.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export default function AppRouter() {
|
|||||||
// navigate if is alias route
|
// navigate if is alias route
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
for (const d of data) {
|
for (const d of data) {
|
||||||
if (`/${d.alias}` === location.pathname && d.enabled) {
|
if (`/${d.alias}` === location.pathname && d.enabled) {
|
||||||
navigate(`/${d.pathAndParams}`);
|
navigate(`/${d.pathAndParams}`);
|
||||||
@@ -48,9 +49,9 @@ export default function AppRouter() {
|
|||||||
}
|
}
|
||||||
}, [data, location, navigate]);
|
}, [data, location, navigate]);
|
||||||
|
|
||||||
return(
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path='/' element={<Navigate to="/timer" /> } />
|
<Route path='/' element={<Navigate to='/timer' />} />
|
||||||
<Route path='/speaker' element={<STimer />} />
|
<Route path='/speaker' element={<STimer />} />
|
||||||
<Route path='/presenter' element={<STimer />} />
|
<Route path='/presenter' element={<STimer />} />
|
||||||
<Route path='/stage' element={<STimer />} />
|
<Route path='/stage' element={<STimer />} />
|
||||||
@@ -111,8 +112,8 @@ export default function AppRouter() {
|
|||||||
</FeatureWrapper>
|
</FeatureWrapper>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{/* Send to default if nothing found */}
|
{/*/!* Send to default if nothing found *!/*/}
|
||||||
<Route path='*' element={<Navigate to="/timer" /> } />
|
<Route path='*' element={<STimer />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { atom } from 'jotai';
|
|
||||||
import { atomWithStorage, selectAtom } from 'jotai/utils';
|
|
||||||
|
|
||||||
export const eventSettingsAtom = atomWithStorage('ontime-eventSettings', {
|
|
||||||
showQuickEntry: false,
|
|
||||||
startTimeIsLastEnd: false,
|
|
||||||
defaultPublic: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const showQuickEntryAtom = selectAtom(
|
|
||||||
eventSettingsAtom,
|
|
||||||
(settings) => settings.showQuickEntry
|
|
||||||
);
|
|
||||||
export const startTimeIsLastEndAtom = selectAtom(
|
|
||||||
eventSettingsAtom,
|
|
||||||
(settings) => settings.startTimeIsLastEnd
|
|
||||||
);
|
|
||||||
export const defaultPublicAtom = selectAtom(
|
|
||||||
eventSettingsAtom,
|
|
||||||
(settings) => settings.defaultPublic
|
|
||||||
);
|
|
||||||
|
|
||||||
export const editorEventId = atom<string | null>(null);
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { atomWithStorage } from 'jotai/utils';
|
|
||||||
|
|
||||||
export const mirrorViewersAtom = atomWithStorage('ontime-viewers-mirrorViewers', false);
|
|
||||||
@@ -35,7 +35,7 @@ export default function TextInput(props: TextInputProps) {
|
|||||||
<Textarea
|
<Textarea
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
size={size}
|
size={size}
|
||||||
resize='none'
|
resize={resize}
|
||||||
variant='ontime-filled'
|
variant='ontime-filled'
|
||||||
{...textAreaProps}
|
{...textAreaProps}
|
||||||
style={{ height: isFullHeight ? '100%' : undefined }}
|
style={{ height: isFullHeight ? '100%' : undefined }}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react';
|
import { FocusEvent, KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { Button, Input, InputGroup, InputLeftElement, Tooltip } from '@chakra-ui/react';
|
import { Button, Input, InputGroup, InputLeftElement, Tooltip } from '@chakra-ui/react';
|
||||||
import { millisToString } from 'ontime-utils';
|
import { millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
@@ -21,9 +21,7 @@ interface TimeInputProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function TimeInput(props: TimeInputProps) {
|
export default function TimeInput(props: TimeInputProps) {
|
||||||
const {
|
const { name, submitHandler, time = 0, delay = 0, placeholder, validationHandler, previousEnd = 0 } = props;
|
||||||
name, submitHandler, time = 0, delay = 0, placeholder, validationHandler, previousEnd = 0,
|
|
||||||
} = props;
|
|
||||||
const { emitError } = useEmitLog();
|
const { emitError } = useEmitLog();
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
@@ -51,74 +49,90 @@ export default function TimeInput(props: TimeInputProps) {
|
|||||||
* @description Submit handler
|
* @description Submit handler
|
||||||
* @param {string} newValue
|
* @param {string} newValue
|
||||||
*/
|
*/
|
||||||
const handleSubmit = useCallback((newValue: string) => {
|
const handleSubmit = useCallback(
|
||||||
// Check if there is anything there
|
(newValue: string) => {
|
||||||
if (newValue === '') {
|
// Check if there is anything there
|
||||||
return false;
|
if (newValue === '') {
|
||||||
}
|
return false;
|
||||||
|
|
||||||
let newValMillis = 0;
|
|
||||||
|
|
||||||
// check for known aliases
|
|
||||||
if (newValue === 'p' || newValue === 'prev' || newValue === 'previous') {
|
|
||||||
// string to pass should be the time of the end before
|
|
||||||
if (previousEnd != null) {
|
|
||||||
newValMillis = previousEnd;
|
|
||||||
}
|
}
|
||||||
} else if (newValue.startsWith('+') || newValue.startsWith('p+') || newValue.startsWith('p +')) {
|
|
||||||
// string to pass should add to the end before
|
|
||||||
const val = newValue.substring(1);
|
|
||||||
newValMillis = previousEnd + forgivingStringToMillis(val);
|
|
||||||
} else {
|
|
||||||
// convert entered value to milliseconds
|
|
||||||
newValMillis = forgivingStringToMillis(newValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Time now and time submittedVal
|
let newValMillis = 0;
|
||||||
const originalMillis = time + delay;
|
|
||||||
|
|
||||||
// check if time is different from before
|
// check for known aliases
|
||||||
if (newValMillis === originalMillis) return false;
|
if (newValue === 'p' || newValue === 'prev' || newValue === 'previous') {
|
||||||
|
// string to pass should be the time of the end before
|
||||||
|
if (previousEnd != null) {
|
||||||
|
newValMillis = previousEnd;
|
||||||
|
}
|
||||||
|
} else if (newValue.startsWith('+') || newValue.startsWith('p+') || newValue.startsWith('p +')) {
|
||||||
|
// string to pass should add to the end before
|
||||||
|
const val = newValue.substring(1);
|
||||||
|
newValMillis = previousEnd + forgivingStringToMillis(val);
|
||||||
|
} else {
|
||||||
|
// convert entered value to milliseconds
|
||||||
|
newValMillis = forgivingStringToMillis(newValue);
|
||||||
|
}
|
||||||
|
|
||||||
// validate with parent
|
// Time now and time submittedVal
|
||||||
if (!validationHandler(name, newValMillis)) return false;
|
const originalMillis = time + delay;
|
||||||
|
|
||||||
// update entry
|
// check if time is different from before
|
||||||
submitHandler(name, newValMillis);
|
if (newValMillis === originalMillis) return false;
|
||||||
|
|
||||||
return true;
|
// validate with parent
|
||||||
}, [delay, name, previousEnd, submitHandler, time, validationHandler]);
|
if (!validationHandler(name, newValMillis)) return false;
|
||||||
|
|
||||||
|
// update entry
|
||||||
|
submitHandler(name, newValMillis);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[delay, name, previousEnd, submitHandler, time, validationHandler],
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Prepare time fields
|
* @description Prepare time fields
|
||||||
* @param {string} value string to be parsed
|
* @param {string} value string to be parsed
|
||||||
*/
|
*/
|
||||||
const validateAndSubmit = useCallback((newValue: string) => {
|
const validateAndSubmit = useCallback(
|
||||||
const success = handleSubmit(newValue);
|
(newValue: string) => {
|
||||||
if (success) {
|
const success = handleSubmit(newValue);
|
||||||
const ms = forgivingStringToMillis(newValue);
|
if (success) {
|
||||||
setValue(millisToString(ms + delay));
|
const ms = forgivingStringToMillis(newValue);
|
||||||
} else {
|
setValue(millisToString(ms + delay));
|
||||||
resetValue();
|
} else {
|
||||||
}
|
resetValue();
|
||||||
}, [delay, handleSubmit, resetValue]);
|
}
|
||||||
|
},
|
||||||
|
[delay, handleSubmit, resetValue],
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Handles common keys for submit and cancel
|
* @description Handles common keys for submit and cancel
|
||||||
* @param {KeyboardEvent} event
|
* @param {KeyboardEvent} event
|
||||||
*/
|
*/
|
||||||
const onKeyDownHandler = useCallback((event:KeyboardEvent<HTMLInputElement>) => {
|
const onKeyDownHandler = useCallback(
|
||||||
if (event.key === 'Enter') {
|
(event: KeyboardEvent<HTMLInputElement>) => {
|
||||||
inputRef.current?.blur();
|
if (event.key === 'Enter') {
|
||||||
|
inputRef.current?.blur();
|
||||||
|
validateAndSubmit((event.target as HTMLInputElement).value);
|
||||||
|
} else if (event.key === 'Tab') {
|
||||||
|
validateAndSubmit((event.target as HTMLInputElement).value);
|
||||||
|
}
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
inputRef.current?.blur();
|
||||||
|
resetValue();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[resetValue, validateAndSubmit],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onBlurHandler = useCallback(
|
||||||
|
(event: FocusEvent<HTMLInputElement>) => {
|
||||||
validateAndSubmit((event.target as HTMLInputElement).value);
|
validateAndSubmit((event.target as HTMLInputElement).value);
|
||||||
} else if (event.key === 'Tab') {
|
},
|
||||||
validateAndSubmit((event.target as HTMLInputElement).value);
|
[validateAndSubmit],
|
||||||
}
|
);
|
||||||
if (event.key === 'Escape') {
|
|
||||||
inputRef.current?.blur();
|
|
||||||
resetValue();
|
|
||||||
}
|
|
||||||
}, [resetValue, validateAndSubmit]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (time == null) return;
|
if (time == null) return;
|
||||||
@@ -167,7 +181,7 @@ export default function TimeInput(props: TimeInputProps) {
|
|||||||
variant='ontime-filled'
|
variant='ontime-filled'
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onChange={(event) => setValue(event.target.value)}
|
onChange={(event) => setValue(event.target.value)}
|
||||||
onBlur={resetValue}
|
onBlur={onBlurHandler}
|
||||||
onKeyDown={onKeyDownHandler}
|
onKeyDown={onKeyDownHandler}
|
||||||
value={value}
|
value={value}
|
||||||
maxLength={8}
|
maxLength={8}
|
||||||
|
|||||||
@@ -6,13 +6,12 @@ import { IoArrowUp } from '@react-icons/all-files/io5/IoArrowUp';
|
|||||||
import { IoContract } from '@react-icons/all-files/io5/IoContract';
|
import { IoContract } from '@react-icons/all-files/io5/IoContract';
|
||||||
import { IoExpand } from '@react-icons/all-files/io5/IoExpand';
|
import { IoExpand } from '@react-icons/all-files/io5/IoExpand';
|
||||||
import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
|
import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
|
||||||
import { useAtom } from 'jotai';
|
|
||||||
|
|
||||||
import { navigatorConstants } from '../../../viewerConfig';
|
import { navigatorConstants } from '../../../viewerConfig';
|
||||||
import { mirrorViewersAtom } from '../../atoms/ViewerSettings';
|
|
||||||
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 { useKeyDown } from '../../hooks/useKeyDown';
|
||||||
|
import { useViewOptionsStore } from '../../stores/viewOptions';
|
||||||
|
|
||||||
import style from './NavigationMenu.module.scss';
|
import style from './NavigationMenu.module.scss';
|
||||||
|
|
||||||
@@ -20,7 +19,7 @@ export default function NavigationMenu() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const { isFullScreen, toggleFullScreen } = useFullscreen();
|
const { isFullScreen, toggleFullScreen } = useFullscreen();
|
||||||
const [isMirrored, setMirrored] = useAtom(mirrorViewersAtom);
|
const { mirror, toggleMirror } = useViewOptionsStore();
|
||||||
const [showButton, setShowButton] = useState(false);
|
const [showButton, setShowButton] = useState(false);
|
||||||
const [showMenu, setShowMenu] = useState(false);
|
const [showMenu, setShowMenu] = useState(false);
|
||||||
const menuRef = useRef<HTMLDivElement | null>(null);
|
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -49,10 +48,10 @@ export default function NavigationMenu() {
|
|||||||
|
|
||||||
const isKeyEnter = (event: KeyboardEvent<HTMLDivElement>) => event.key === 'Enter';
|
const isKeyEnter = (event: KeyboardEvent<HTMLDivElement>) => event.key === 'Enter';
|
||||||
const handleFullscreen = () => toggleFullScreen();
|
const handleFullscreen = () => toggleFullScreen();
|
||||||
const handleMirror = () => setMirrored((prev) => !prev);
|
const handleMirror = () => toggleMirror();
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div id='navigation-menu-portal' ref={menuRef} className={isMirrored ? style.mirror : ''}>
|
<div id='navigation-menu-portal' ref={menuRef} className={mirror ? style.mirror : ''}>
|
||||||
<button
|
<button
|
||||||
onClick={toggleMenu}
|
onClick={toggleMenu}
|
||||||
aria-label='toggle menu'
|
aria-label='toggle menu'
|
||||||
@@ -83,7 +82,8 @@ export default function NavigationMenu() {
|
|||||||
onClick={handleMirror}
|
onClick={handleMirror}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
isKeyEnter(event) && handleMirror();
|
isKeyEnter(event) && handleMirror();
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
Flip Screen
|
Flip Screen
|
||||||
<IoSwapVertical />
|
<IoSwapVertical />
|
||||||
</div>
|
</div>
|
||||||
@@ -97,12 +97,15 @@ export default function NavigationMenu() {
|
|||||||
key={route.url}
|
key={route.url}
|
||||||
to={route.url}
|
to={route.url}
|
||||||
className={`${style.link} ${route.url === location.pathname ? style.current : undefined}`}
|
className={`${style.link} ${route.url === location.pathname ? style.current : undefined}`}
|
||||||
tabIndex={0}>
|
tabIndex={0}
|
||||||
|
>
|
||||||
{route.label}
|
{route.label}
|
||||||
<IoArrowUp className={style.linkIcon} />
|
<IoArrowUp className={style.linkIcon} />
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>, document.body);
|
</div>,
|
||||||
|
document.body,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,23 +17,9 @@ export default function Schedule({ className }: ScheduleProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let selectedState: 'past' | 'now' | 'future' = 'past';
|
let selectedState: 'past' | 'now' | 'future' = 'past';
|
||||||
const selectedEvent = paginatedEvents.find((event) => event.id === selectedEventId);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className={`schedule ${className}`}>
|
<ul className={`schedule ${className}`}>
|
||||||
{selectedEvent && (
|
|
||||||
<ScheduleItem
|
|
||||||
key={selectedEvent.id}
|
|
||||||
selected='now'
|
|
||||||
timeStart={selectedEvent.timeStart}
|
|
||||||
timeEnd={selectedEvent.timeEnd}
|
|
||||||
title={selectedEvent.title}
|
|
||||||
presenter={selectedEvent.presenter}
|
|
||||||
colour={isBackstage ? selectedEvent.colour : ''}
|
|
||||||
backstageEvent={!selectedEvent.isPublic}
|
|
||||||
skip={selectedEvent.skip}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{paginatedEvents.map((event) => {
|
{paginatedEvents.map((event) => {
|
||||||
if (event.id === selectedEventId) {
|
if (event.id === selectedEventId) {
|
||||||
selectedState = 'now';
|
selectedState = 'now';
|
||||||
|
|||||||
@@ -17,4 +17,4 @@ export default function useEventData() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return { data, status, isError, refetch };
|
return { data, status, isError, refetch };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import axios, { AxiosError } from 'axios';
|
import axios, { AxiosError } from 'axios';
|
||||||
import { useAtomValue } from 'jotai';
|
|
||||||
import { OntimeRundown, OntimeRundownEntry, SupportedEvent } from 'ontime-types';
|
import { OntimeRundown, OntimeRundownEntry, SupportedEvent } from 'ontime-types';
|
||||||
|
|
||||||
import { RUNDOWN_TABLE, RUNDOWN_TABLE_KEY } from '../api/apiConstants';
|
import { RUNDOWN_TABLE, RUNDOWN_TABLE_KEY } from '../api/apiConstants';
|
||||||
@@ -14,7 +13,7 @@ import {
|
|||||||
requestPutEvent,
|
requestPutEvent,
|
||||||
requestReorderEvent,
|
requestReorderEvent,
|
||||||
} from '../api/eventsApi';
|
} from '../api/eventsApi';
|
||||||
import { defaultPublicAtom, startTimeIsLastEndAtom } from '../atoms/LocalEventSettings';
|
import { useLocalEvent } from '../stores/localEvent';
|
||||||
import { useEmitLog } from '../stores/logger';
|
import { useEmitLog } from '../stores/logger';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,8 +22,9 @@ import { useEmitLog } from '../stores/logger';
|
|||||||
export const useEventAction = () => {
|
export const useEventAction = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { emitError } = useEmitLog();
|
const { emitError } = useEmitLog();
|
||||||
const defaultPublic = useAtomValue(defaultPublicAtom);
|
const { eventSettings } = useLocalEvent();
|
||||||
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
|
const defaultPublic = eventSettings.defaultPublic;
|
||||||
|
const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls mutation to add new event
|
* Calls mutation to add new event
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
// Roughly from useHooks - useLocalStorage
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description utility hook to handle state in local storage
|
|
||||||
* @param key
|
|
||||||
* @param initialValue
|
|
||||||
*/
|
|
||||||
export const useLocalStorage = (key, initialValue) => {
|
|
||||||
const [storedValue, setStoredValue] = useState(() => {
|
|
||||||
try {
|
|
||||||
const item = window.localStorage.getItem(`ontime-${key}`);
|
|
||||||
return item ? JSON.parse(item) : initialValue;
|
|
||||||
} catch (error) {
|
|
||||||
return initialValue;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Set value to local storage
|
|
||||||
* @param value
|
|
||||||
*/
|
|
||||||
const setValue = (value) => {
|
|
||||||
try {
|
|
||||||
// Allow value to be a function so we have same API as useState
|
|
||||||
const valueToStore =
|
|
||||||
value instanceof Function ? value(storedValue) : value;
|
|
||||||
|
|
||||||
setStoredValue(valueToStore);
|
|
||||||
window.localStorage.setItem(`ontime-${key}`, JSON.stringify(valueToStore));
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return [storedValue, setValue];
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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(() => {
|
||||||
|
const handleStorageChange = (event: StorageEvent) => {
|
||||||
|
if (event.storageArea === window.localStorage && event.key === key) {
|
||||||
|
try {
|
||||||
|
const newValue = event.newValue ? JSON.parse(event.newValue) : initialValue;
|
||||||
|
setStoredValue(newValue);
|
||||||
|
} catch (_) {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('storage', handleStorageChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('storage', handleStorageChange);
|
||||||
|
};
|
||||||
|
}, [initialValue, key]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Set value to local storage
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
const setValue = (value: T | ((val: T) => T)) => {
|
||||||
|
try {
|
||||||
|
// Allow value to be a function so we have same API as useState
|
||||||
|
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
||||||
|
|
||||||
|
setStoredValue(valueToStore);
|
||||||
|
window.localStorage.setItem(`ontime-${key}`, JSON.stringify(valueToStore));
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return [storedValue, setValue];
|
||||||
|
};
|
||||||
@@ -77,7 +77,10 @@ export const useInfoPanel = () => {
|
|||||||
|
|
||||||
export const useCuesheet = () => {
|
export const useCuesheet = () => {
|
||||||
const featureSelector = (state: RuntimeStore) => ({
|
const featureSelector = (state: RuntimeStore) => ({
|
||||||
selectedEventIndex: state.loaded.selectedEventId,
|
playback: state.playback,
|
||||||
|
selectedEventId: state.loaded.selectedEventId,
|
||||||
|
selectedEventIndex: state.loaded.selectedEventIndex,
|
||||||
|
numEvents: state.loaded.numEvents,
|
||||||
titleNow: state.titles.titleNow,
|
titleNow: state.titles.titleNow,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -92,7 +95,7 @@ export const setEventPlayback = {
|
|||||||
|
|
||||||
export const useTimer = () => {
|
export const useTimer = () => {
|
||||||
const featureSelector = (state: RuntimeStore) => ({
|
const featureSelector = (state: RuntimeStore) => ({
|
||||||
timer: state.timer,
|
...state.timer,
|
||||||
});
|
});
|
||||||
|
|
||||||
return useRuntimeStore(featureSelector, deepCompare);
|
return useRuntimeStore(featureSelector, deepCompare);
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
export default function createStore<T>(initialState: T) {
|
|
||||||
let currentState = initialState;
|
|
||||||
const listeners = new Set<(state: T) => void>();
|
|
||||||
|
|
||||||
return {
|
|
||||||
get: () => currentState,
|
|
||||||
set: (newState: T) => {
|
|
||||||
currentState = newState;
|
|
||||||
listeners.forEach((listener) => listener(currentState));
|
|
||||||
},
|
|
||||||
subscribe: (listener: (state: T) => void) => {
|
|
||||||
listeners.add(listener);
|
|
||||||
return () => listeners.delete(listener);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
type EventEditorStore = {
|
||||||
|
openId: string | null;
|
||||||
|
setOpenEvent: (eventId: string) => void;
|
||||||
|
removeOpenEvent: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useEventEditorStore = create<EventEditorStore>()((set) => ({
|
||||||
|
openId: null,
|
||||||
|
setOpenEvent: (eventId: string | null) => set({ openId: eventId }),
|
||||||
|
removeOpenEvent: () => set({ openId: null }),
|
||||||
|
}));
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
import { booleanFromLocalStorage } from '../utils/localStorage';
|
||||||
|
|
||||||
|
type EventSettings = {
|
||||||
|
showQuickEntry: boolean;
|
||||||
|
startTimeIsLastEnd: boolean;
|
||||||
|
defaultPublic: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LocalEventStore = {
|
||||||
|
eventSettings: EventSettings;
|
||||||
|
setLocalEventSettings: (newState: EventSettings) => void;
|
||||||
|
setShowQuickEntry: (showQuickEntry: boolean) => void;
|
||||||
|
setStartTimeIsLastEnd: (startTimeIsLastEnd: boolean) => void;
|
||||||
|
setDefaultPublic: (defaultPublic: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum LocalEventKeys {
|
||||||
|
ShowQuickEntry = 'ontime-show-quick-entry',
|
||||||
|
StartTimeIsLastEnd = 'ontime-start-is-last-end',
|
||||||
|
DefaultPublic = 'ontime-default-public',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useLocalEvent = create<LocalEventStore>((set) => ({
|
||||||
|
eventSettings: {
|
||||||
|
showQuickEntry: booleanFromLocalStorage(LocalEventKeys.ShowQuickEntry, false),
|
||||||
|
startTimeIsLastEnd: booleanFromLocalStorage(LocalEventKeys.ShowQuickEntry, true),
|
||||||
|
defaultPublic: booleanFromLocalStorage(LocalEventKeys.ShowQuickEntry, true),
|
||||||
|
},
|
||||||
|
|
||||||
|
setLocalEventSettings: (value) =>
|
||||||
|
set(() => {
|
||||||
|
localStorage.setItem(LocalEventKeys.ShowQuickEntry, String(value.showQuickEntry));
|
||||||
|
localStorage.setItem(LocalEventKeys.StartTimeIsLastEnd, String(value.startTimeIsLastEnd));
|
||||||
|
localStorage.setItem(LocalEventKeys.DefaultPublic, String(value.defaultPublic));
|
||||||
|
return { eventSettings: value };
|
||||||
|
}),
|
||||||
|
|
||||||
|
setShowQuickEntry: (showQuickEntry) =>
|
||||||
|
set((state) => {
|
||||||
|
localStorage.setItem(LocalEventKeys.ShowQuickEntry, String(showQuickEntry));
|
||||||
|
return { eventSettings: { ...state.eventSettings, showQuickEntry } };
|
||||||
|
}),
|
||||||
|
|
||||||
|
setStartTimeIsLastEnd: (startTimeIsLastEnd) =>
|
||||||
|
set((state) => {
|
||||||
|
localStorage.setItem(LocalEventKeys.StartTimeIsLastEnd, String(startTimeIsLastEnd));
|
||||||
|
return { eventSettings: { ...state.eventSettings, startTimeIsLastEnd } };
|
||||||
|
}),
|
||||||
|
|
||||||
|
setDefaultPublic: (defaultPublic) =>
|
||||||
|
set((state) => {
|
||||||
|
localStorage.setItem(LocalEventKeys.DefaultPublic, String(defaultPublic));
|
||||||
|
return { eventSettings: { ...state.eventSettings, defaultPublic } };
|
||||||
|
}),
|
||||||
|
}));
|
||||||
@@ -19,7 +19,7 @@ export const useLogData = () => useStore(logger);
|
|||||||
|
|
||||||
export const addLog = (log: Log) =>
|
export const addLog = (log: Log) =>
|
||||||
logger.setState((state) => ({
|
logger.setState((state) => ({
|
||||||
logs: [...state.logs, log],
|
logs: [log, ...state.logs],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const clearLogs = () => logger.setState({ logs: [] });
|
export const clearLogs = () => logger.setState({ logs: [] });
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const runtimeStorePlaceholder = {
|
|||||||
selectedEventId: null,
|
selectedEventId: null,
|
||||||
duration: null,
|
duration: null,
|
||||||
timerType: null,
|
timerType: null,
|
||||||
|
endAction: null,
|
||||||
},
|
},
|
||||||
playback: Playback.Stop,
|
playback: Playback.Stop,
|
||||||
timerMessage: {
|
timerMessage: {
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
import { booleanFromLocalStorage } from '../utils/localStorage';
|
||||||
|
|
||||||
|
enum LocalEventKeys {
|
||||||
|
Mirror = 'ontime-view-mirror',
|
||||||
|
}
|
||||||
|
|
||||||
|
type ViewOptionsStore = {
|
||||||
|
mirror: boolean;
|
||||||
|
toggleMirror: (newValue?: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useViewOptionsStore = create<ViewOptionsStore>()((set) => ({
|
||||||
|
mirror: booleanFromLocalStorage(LocalEventKeys.Mirror, false),
|
||||||
|
toggleMirror: (newValue?: boolean) =>
|
||||||
|
set((state) => {
|
||||||
|
const val = typeof newValue === 'undefined' ? !state.mirror : newValue;
|
||||||
|
localStorage.setItem(LocalEventKeys.Mirror, String(val));
|
||||||
|
return { mirror: val };
|
||||||
|
}),
|
||||||
|
}));
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export function booleanFromLocalStorage(key: string, fallback: boolean): boolean {
|
||||||
|
const valueInStorage = localStorage.getItem(key);
|
||||||
|
if (valueInStorage) {
|
||||||
|
return valueInStorage === 'true';
|
||||||
|
} else {
|
||||||
|
localStorage.setItem(key, String(fallback));
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Tooltip } from '@chakra-ui/react';
|
import { Tooltip } from '@chakra-ui/react';
|
||||||
import { Playback } from 'ontime-types';
|
import { Playback } from 'ontime-types';
|
||||||
|
import { millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
import TimerDisplay from '../../../common/components/timer-display/TimerDisplay';
|
import TimerDisplay from '../../../common/components/timer-display/TimerDisplay';
|
||||||
import { setPlayback, useTimer } from '../../../common/hooks/useSocket';
|
import { setPlayback, useTimer } from '../../../common/hooks/useSocket';
|
||||||
@@ -9,7 +10,6 @@ import { tooltipDelayMid } from '../../../ontimeConfig';
|
|||||||
import TapButton from './TapButton';
|
import TapButton from './TapButton';
|
||||||
|
|
||||||
import style from './PlaybackControl.module.scss';
|
import style from './PlaybackControl.module.scss';
|
||||||
import { millisToString } from 'ontime-utils';
|
|
||||||
|
|
||||||
interface PlaybackTimerProps {
|
interface PlaybackTimerProps {
|
||||||
playback: Playback;
|
playback: Playback;
|
||||||
@@ -17,20 +17,20 @@ interface PlaybackTimerProps {
|
|||||||
|
|
||||||
export default function PlaybackTimer(props: PlaybackTimerProps) {
|
export default function PlaybackTimer(props: PlaybackTimerProps) {
|
||||||
const { playback } = props;
|
const { playback } = props;
|
||||||
const data = useTimer();
|
const timer = useTimer();
|
||||||
|
|
||||||
// TODO: checkout typescript in utilities
|
// TODO: checkout typescript in utilities
|
||||||
const started = millisToString(data.timer.startedAt);
|
const started = millisToString(timer.startedAt);
|
||||||
const finish = millisToString(data.timer.expectedFinish);
|
const finish = millisToString(timer.expectedFinish);
|
||||||
const isRolling = playback === Playback.Roll;
|
const isRolling = playback === Playback.Roll;
|
||||||
const isStopped = playback === Playback.Stop;
|
const isStopped = playback === Playback.Stop;
|
||||||
const isWaiting = data.timer.secondaryTimer !== null && data.timer.secondaryTimer > 0 && data.timer.current === null;
|
const isWaiting = timer.secondaryTimer !== null && timer.secondaryTimer > 0 && timer.current === null;
|
||||||
const disableButtons = isStopped || isRolling;
|
const disableButtons = isStopped || isRolling;
|
||||||
const isOvertime = data.timer.current !== null && data.timer.current < 0;
|
const isOvertime = timer.current !== null && timer.current < 0;
|
||||||
const hasAddedTime = Boolean(data.timer.addedTime);
|
const hasAddedTime = Boolean(timer.addedTime);
|
||||||
|
|
||||||
const rollLabel = isRolling ? 'Roll mode active' : '';
|
const rollLabel = isRolling ? 'Roll mode active' : '';
|
||||||
const addedTimeLabel = hasAddedTime ? `Added ${millisToMinutes(data.timer.addedTime)} minutes` : '';
|
const addedTimeLabel = hasAddedTime ? `Added ${millisToMinutes(timer.addedTime)} minutes` : '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.timeContainer}>
|
<div className={style.timeContainer}>
|
||||||
@@ -44,7 +44,7 @@ export default function PlaybackTimer(props: PlaybackTimerProps) {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.timer}>
|
<div className={style.timer}>
|
||||||
<TimerDisplay time={isWaiting ? data.timer.secondaryTimer : data.timer.current} />
|
<TimerDisplay time={isWaiting ? timer.secondaryTimer : timer.current} />
|
||||||
</div>
|
</div>
|
||||||
{isWaiting ? (
|
{isWaiting ? (
|
||||||
<div className={style.roll}>
|
<div className={style.roll}>
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { Box, useDisclosure } from '@chakra-ui/react';
|
|||||||
|
|
||||||
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
||||||
import UploadModal from '../../common/components/upload-modal/UploadModal';
|
import UploadModal from '../../common/components/upload-modal/UploadModal';
|
||||||
import ModalManager from '../modals/ModalManager';
|
|
||||||
import MenuBar from '../menu/MenuBar';
|
import MenuBar from '../menu/MenuBar';
|
||||||
import IntegrationModal from '../modals/integration-modal/IntegrationModal';
|
import IntegrationModal from '../modals/integration-modal/IntegrationModal';
|
||||||
|
import ModalManager from '../modals/ModalManager';
|
||||||
|
|
||||||
import styles from './Editor.module.scss';
|
import styles from './Editor.module.scss';
|
||||||
|
|
||||||
@@ -17,9 +17,7 @@ const EventEditor = lazy(() => import('../../features/event-editor/EventEditorEx
|
|||||||
|
|
||||||
export default function Editor() {
|
export default function Editor() {
|
||||||
const { isOpen: isSettingsOpen, onOpen: onSettingsOpen, onClose: onSettingsClose } = useDisclosure();
|
const { isOpen: isSettingsOpen, onOpen: onSettingsOpen, onClose: onSettingsClose } = useDisclosure();
|
||||||
|
|
||||||
const { isOpen: isUploadModalOpen, onOpen: onUploadModalOpen, onClose: onUploadModalClose } = useDisclosure();
|
const { isOpen: isUploadModalOpen, onOpen: onUploadModalOpen, onClose: onUploadModalClose } = useDisclosure();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isOpen: isIntegrationModalOpen,
|
isOpen: isIntegrationModalOpen,
|
||||||
onOpen: onIntegrationModalOpen,
|
onOpen: onIntegrationModalOpen,
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { Button, Select, Switch } from '@chakra-ui/react';
|
import { Button, Select, Switch } from '@chakra-ui/react';
|
||||||
import { IoBan } from '@react-icons/all-files/io5/IoBan';
|
import { IoBan } from '@react-icons/all-files/io5/IoBan';
|
||||||
import { useAtom } from 'jotai';
|
|
||||||
import { EndAction, OntimeEvent, TimerType } from 'ontime-types';
|
import { EndAction, OntimeEvent, TimerType } from 'ontime-types';
|
||||||
import { millisToString } from 'ontime-utils';
|
import { millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
import { editorEventId } from '../../common/atoms/LocalEventSettings';
|
|
||||||
import CopyTag from '../../common/components/copy-tag/CopyTag';
|
import CopyTag from '../../common/components/copy-tag/CopyTag';
|
||||||
import ColourInput from '../../common/components/input/colour-input/ColourInput';
|
import ColourInput from '../../common/components/input/colour-input/ColourInput';
|
||||||
import TextInput from '../../common/components/input/text-input/TextInput';
|
import TextInput from '../../common/components/input/text-input/TextInput';
|
||||||
import TimeInput from '../../common/components/input/time-input/TimeInput';
|
import TimeInput from '../../common/components/input/time-input/TimeInput';
|
||||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||||
import useRundown from '../../common/hooks-query/useRundown';
|
import useRundown from '../../common/hooks-query/useRundown';
|
||||||
|
import { useEventEditorStore } from '../../common/stores/eventEditor';
|
||||||
import { useEmitLog } from '../../common/stores/logger';
|
import { useEmitLog } from '../../common/stores/logger';
|
||||||
import { millisToMinutes } from '../../common/utils/dateConfig';
|
import { millisToMinutes } from '../../common/utils/dateConfig';
|
||||||
import getDelayTo from '../../common/utils/getDelayTo';
|
import getDelayTo from '../../common/utils/getDelayTo';
|
||||||
@@ -23,7 +22,7 @@ export type EventEditorSubmitActions = keyof OntimeEvent | 'durationOverride';
|
|||||||
|
|
||||||
// Todo: add previous end to TimeInput fields
|
// Todo: add previous end to TimeInput fields
|
||||||
export default function EventEditor() {
|
export default function EventEditor() {
|
||||||
const [openId] = useAtom(editorEventId);
|
const { openId } = useEventEditorStore();
|
||||||
const { data } = useRundown();
|
const { data } = useRundown();
|
||||||
const { emitWarning, emitError } = useEmitLog();
|
const { emitWarning, emitError } = useEmitLog();
|
||||||
const { updateEvent } = useEventAction();
|
const { updateEvent } = useEventAction();
|
||||||
@@ -226,7 +225,14 @@ export default function EventEditor() {
|
|||||||
</div>
|
</div>
|
||||||
<div className={`${style.column} ${style.fullHeight}`}>
|
<div className={`${style.column} ${style.fullHeight}`}>
|
||||||
<label className={style.inputLabel}>Note</label>
|
<label className={style.inputLabel}>Note</label>
|
||||||
<TextInput field='note' initialText={event.note} submitHandler={handleSubmit} isTextArea isFullHeight />
|
<TextInput
|
||||||
|
field='note'
|
||||||
|
initialText={event.note}
|
||||||
|
submitHandler={handleSubmit}
|
||||||
|
isTextArea
|
||||||
|
isFullHeight
|
||||||
|
resize='none'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Box, IconButton } from '@chakra-ui/react';
|
import { Box, IconButton } from '@chakra-ui/react';
|
||||||
import { FiX } from '@react-icons/all-files/fi/FiX';
|
import { FiX } from '@react-icons/all-files/fi/FiX';
|
||||||
import { editorEventId } from '../../common/atoms/LocalEventSettings';
|
|
||||||
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
||||||
import { useAtom } from 'jotai';
|
import { useEventEditorStore } from '../../common/stores/eventEditor';
|
||||||
|
|
||||||
import EventEditor from './EventEditor';
|
import EventEditor from './EventEditor';
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ const closeBtnStyle = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function InfoExport() {
|
export default function InfoExport() {
|
||||||
const [openId, setOpenId] = useAtom(editorEventId);
|
const { openId, removeOpenEvent } = useEventEditorStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={`${style.eventEditor} ${!openId ? style.noEvent : ''}`}>
|
<Box className={`${style.eventEditor} ${!openId ? style.noEvent : ''}`}>
|
||||||
@@ -25,14 +25,8 @@ export default function InfoExport() {
|
|||||||
<div className={style.eventEditorLayout}>
|
<div className={style.eventEditorLayout}>
|
||||||
<EventEditor />
|
<EventEditor />
|
||||||
<div className={style.header}>
|
<div className={style.header}>
|
||||||
<IconButton
|
<IconButton aria-label='Close Menu' icon={<FiX />} onClick={removeOpenEvent} {...closeBtnStyle} />
|
||||||
aria-label='Close Menu'
|
|
||||||
icon={<FiX />}
|
|
||||||
onClick={() => setOpenId(null)}
|
|
||||||
{...closeBtnStyle}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -14,16 +14,15 @@ import {
|
|||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { FiEye } from '@react-icons/all-files/fi/FiEye';
|
import { FiEye } from '@react-icons/all-files/fi/FiEye';
|
||||||
import { FiX } from '@react-icons/all-files/fi/FiX';
|
import { FiX } from '@react-icons/all-files/fi/FiX';
|
||||||
import { useAtom } from 'jotai';
|
|
||||||
|
|
||||||
import { useEmitLog } from '@/common/stores/logger';
|
import { useEmitLog } from '@/common/stores/logger';
|
||||||
|
|
||||||
import { version } from '../../../package.json';
|
import { version } from '../../../package.json';
|
||||||
import { getLatestVersion, postSettings } from '../../common/api/ontimeApi';
|
import { getLatestVersion, postSettings } from '../../common/api/ontimeApi';
|
||||||
import { eventSettingsAtom } from '../../common/atoms/LocalEventSettings';
|
|
||||||
import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn';
|
import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn';
|
||||||
import useSettings from '../../common/hooks-query/useSettings';
|
import useSettings from '../../common/hooks-query/useSettings';
|
||||||
import { ontimePlaceholderSettings } from '../../common/models/OntimeSettings';
|
import { ontimePlaceholderSettings } from '../../common/models/OntimeSettings';
|
||||||
|
import { useLocalEvent } from '../../common/stores/localEvent';
|
||||||
|
|
||||||
import { inputProps } from './modalHelper';
|
import { inputProps } from './modalHelper';
|
||||||
import SubmitContainer from './SubmitContainer';
|
import SubmitContainer from './SubmitContainer';
|
||||||
@@ -38,7 +37,7 @@ export default function AppSettingsModal() {
|
|||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [hidePin, setHidePin] = useState(true);
|
const [hidePin, setHidePin] = useState(true);
|
||||||
|
|
||||||
const [eventSettings, saveEventSettings] = useAtom(eventSettingsAtom);
|
const { eventSettings, setLocalEventSettings } = useLocalEvent();
|
||||||
const [formSettings, setFormSettings] = useState(eventSettings);
|
const [formSettings, setFormSettings] = useState(eventSettings);
|
||||||
|
|
||||||
const [updateMessage, setUpdateMessage] = useState(<a>Using ontime version: {version}</a>);
|
const [updateMessage, setUpdateMessage] = useState(<a>Using ontime version: {version}</a>);
|
||||||
@@ -69,7 +68,7 @@ export default function AppSettingsModal() {
|
|||||||
|
|
||||||
const hasChanged = !isEqual(formSettings, eventSettings);
|
const hasChanged = !isEqual(formSettings, eventSettings);
|
||||||
if (hasChanged) {
|
if (hasChanged) {
|
||||||
saveEventSettings(formSettings);
|
setLocalEventSettings(formSettings);
|
||||||
validation.isValid = true;
|
validation.isValid = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,359 +0,0 @@
|
|||||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
|
||||||
import { FormControl, FormLabel, Input, ModalBody, Switch } from '@chakra-ui/react';
|
|
||||||
import { FiInfo } from '@react-icons/all-files/fi/FiInfo';
|
|
||||||
|
|
||||||
import { LoggingContext } from '../../common/context/LoggingContext';
|
|
||||||
import useInfo from '../../common/hooks-query/useInfo';
|
|
||||||
import { httpPlaceholder } from '../../common/models/Http';
|
|
||||||
import { ontimeVars } from '../../common/models/OntimeVars';
|
|
||||||
|
|
||||||
import { inputProps } from './modalHelper';
|
|
||||||
import SubmitContainer from './SubmitContainer';
|
|
||||||
|
|
||||||
import style from './Modals.module.scss';
|
|
||||||
|
|
||||||
export default function IntegrationSettingsModal() {
|
|
||||||
const { data, status, refetch } = useInfo();
|
|
||||||
const { emitError } = useContext(LoggingContext);
|
|
||||||
const [formData, setFormData] = useState(httpPlaceholder);
|
|
||||||
const [changed, setChanged] = useState(false);
|
|
||||||
const [submitting, setSubmitting] = useState(false);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set formdata from server state
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
if (data == null) return;
|
|
||||||
if (changed) return;
|
|
||||||
|
|
||||||
setFormData({
|
|
||||||
onLoad: data?.onLoad,
|
|
||||||
onStart: data?.onStart,
|
|
||||||
onUpdate: data?.onUpdate,
|
|
||||||
onPause: data?.onPause,
|
|
||||||
onStop: data?.onStop,
|
|
||||||
});
|
|
||||||
}, [changed, data]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate and submit data
|
|
||||||
*/
|
|
||||||
const submitHandler = useCallback(
|
|
||||||
async (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
const f = formData;
|
|
||||||
const e = { status: false, message: '' };
|
|
||||||
|
|
||||||
// set fields with error
|
|
||||||
if (e.status) {
|
|
||||||
emitError(`Invalid Input: ${e.message}`);
|
|
||||||
} else {
|
|
||||||
// call API endpoint here with value of f
|
|
||||||
setChanged(false);
|
|
||||||
setSubmitting(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[emitError, formData],
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverts local state equals to server state
|
|
||||||
*/
|
|
||||||
const revert = useCallback(async () => {
|
|
||||||
setChanged(false);
|
|
||||||
await refetch();
|
|
||||||
}, [refetch]);
|
|
||||||
|
|
||||||
// Todo: make change handler
|
|
||||||
// Todo: toggle between GET / POST
|
|
||||||
// Todo: add test button
|
|
||||||
// Todo: enabled should be button
|
|
||||||
// Todo: add friendly placeholder to input
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalBody className={style.modalBody}>
|
|
||||||
<p className={style.notes}>
|
|
||||||
Integrate with third party over an HTTP API
|
|
||||||
<br />
|
|
||||||
🔥 Changes take effect after app restart 🔥
|
|
||||||
</p>
|
|
||||||
<form onSubmit={submitHandler}>
|
|
||||||
<div className={style.modalFields}>
|
|
||||||
<div className={style.hSeparator}>Ontime event cycle</div>
|
|
||||||
<div className={style.blockNotes}>
|
|
||||||
<span className={style.inlineFlex}>
|
|
||||||
<FiInfo color='#2b6cb0' fontSize='2em' />
|
|
||||||
Add HTTP messages that ontime will send during the event cycle
|
|
||||||
</span>
|
|
||||||
<span className={style.labelNote}>
|
|
||||||
You can use variables in the HTTP request URL to send data from ontime
|
|
||||||
</span>
|
|
||||||
<span className={style.emNote}>
|
|
||||||
http://127.0.0.1:8088/API/?setHeadline=
|
|
||||||
<span className={style.labelNoteInline}>$title</span>
|
|
||||||
&setSub=<span className={style.labelNoteInline}>$presenter</span>
|
|
||||||
</span>
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td className={style.labelNote} style={{ width: '30%' }}>
|
|
||||||
Variable
|
|
||||||
</td>
|
|
||||||
<td className={style.labelNote}>Value</td>
|
|
||||||
</tr>
|
|
||||||
{ontimeVars.map((v) => (
|
|
||||||
<tr key={v.name}>
|
|
||||||
<td className={style.labelNote}>{v.name}</td>
|
|
||||||
<td>{v.description}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div className={style.hSeparator}>Send HTTP</div>
|
|
||||||
<FormLabel style={{ paddingLeft: '0.5em' }}>
|
|
||||||
On Load
|
|
||||||
<span className={style.labelNote}>
|
|
||||||
<br />
|
|
||||||
When a new event loads
|
|
||||||
</span>
|
|
||||||
</FormLabel>
|
|
||||||
<div className={style.modalInline}>
|
|
||||||
<Input
|
|
||||||
{...inputProps}
|
|
||||||
name='onLoadURL'
|
|
||||||
value={formData?.onLoad?.url}
|
|
||||||
onChange={(event) => {
|
|
||||||
setChanged(true);
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
onLoad: {
|
|
||||||
...formData.onLoad,
|
|
||||||
url: event.target.value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Switch
|
|
||||||
colorScheme='green'
|
|
||||||
id='onLoadEnable'
|
|
||||||
value={formData?.onLoad?.enabled}
|
|
||||||
onChange={(event) => {
|
|
||||||
setChanged(true);
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
onLoad: {
|
|
||||||
...formData.onLoad,
|
|
||||||
enabled: event.target.value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<FormLabel style={{ paddingLeft: '0.5em' }}>
|
|
||||||
On Start
|
|
||||||
<span className={style.labelNote}>
|
|
||||||
<br />
|
|
||||||
When an timer starts / resumes{' '}
|
|
||||||
</span>
|
|
||||||
</FormLabel>
|
|
||||||
<div className={style.modalInline}>
|
|
||||||
<Input
|
|
||||||
{...inputProps}
|
|
||||||
name='onStartURL'
|
|
||||||
value={formData?.onStart?.url}
|
|
||||||
onChange={(event) => {
|
|
||||||
setChanged(true);
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
onStart: {
|
|
||||||
...formData.onStart,
|
|
||||||
url: event.target.value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Switch
|
|
||||||
colorScheme='green'
|
|
||||||
id='onStartEnable'
|
|
||||||
value={formData?.onStart?.enabled}
|
|
||||||
onChange={(event) => {
|
|
||||||
setChanged(true);
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
onStart: {
|
|
||||||
...formData.onStart,
|
|
||||||
enabled: event.target.value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<FormLabel style={{ paddingLeft: '0.5em' }}>
|
|
||||||
On Update
|
|
||||||
<span className={style.labelNote}>
|
|
||||||
<br />
|
|
||||||
At every clock tick
|
|
||||||
</span>
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl id='onUpdate' className={style.modalInline}>
|
|
||||||
<Input
|
|
||||||
{...inputProps}
|
|
||||||
name='onUpdateURL'
|
|
||||||
value={formData?.onUpdate?.url}
|
|
||||||
onChange={(event) => {
|
|
||||||
setChanged(true);
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
onUpdate: {
|
|
||||||
...formData.onUpdate,
|
|
||||||
url: event.target.value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Switch
|
|
||||||
colorScheme='green'
|
|
||||||
id='onUpdateEnable'
|
|
||||||
value={formData?.onUpdate?.enabled}
|
|
||||||
onChange={(event) => {
|
|
||||||
setChanged(true);
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
onUpdate: {
|
|
||||||
...formData.onUpdate,
|
|
||||||
enabled: event.target.value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormLabel style={{ paddingLeft: '0.5em' }}>
|
|
||||||
On Pause
|
|
||||||
<span className={style.labelNote}>
|
|
||||||
<br />
|
|
||||||
When a timer pauses
|
|
||||||
</span>
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl id='onPause' className={style.modalInline}>
|
|
||||||
<Input
|
|
||||||
{...inputProps}
|
|
||||||
name='onPauseURL'
|
|
||||||
value={formData?.onPause?.url}
|
|
||||||
onChange={(event) => {
|
|
||||||
setChanged(true);
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
onPause: {
|
|
||||||
...formData.onPause,
|
|
||||||
url: event.target.value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Switch
|
|
||||||
colorScheme='green'
|
|
||||||
id='onPauseEnable'
|
|
||||||
value={formData?.onPause?.enabled}
|
|
||||||
onChange={(event) => {
|
|
||||||
setChanged(true);
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
onPause: {
|
|
||||||
...formData.onPause,
|
|
||||||
enabled: event.target.value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormLabel style={{ paddingLeft: '0.5em' }}>
|
|
||||||
On Stop
|
|
||||||
<span className={style.labelNote}>
|
|
||||||
<br />
|
|
||||||
When an event is unloaded
|
|
||||||
</span>
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl id='onStop' className={style.modalInline}>
|
|
||||||
<Input
|
|
||||||
{...inputProps}
|
|
||||||
name='onStopURL'
|
|
||||||
value={formData?.onStop?.url}
|
|
||||||
onChange={(event) => {
|
|
||||||
setChanged(true);
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
onStop: {
|
|
||||||
...formData.onStop,
|
|
||||||
url: event.target.value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Switch
|
|
||||||
colorScheme='green'
|
|
||||||
id='onStopEnable'
|
|
||||||
value={formData?.onStop?.enabled}
|
|
||||||
onChange={(event) => {
|
|
||||||
setChanged(true);
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
onStop: {
|
|
||||||
...formData.onStop,
|
|
||||||
enabled: event.target.value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormLabel style={{ paddingLeft: '0.5em' }}>
|
|
||||||
On Finish
|
|
||||||
<span className={style.labelNote}>
|
|
||||||
<br />
|
|
||||||
When an event is finished
|
|
||||||
</span>
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl id='onFinish' className={style.modalInline}>
|
|
||||||
<Input
|
|
||||||
{...inputProps}
|
|
||||||
name='onFinishURL'
|
|
||||||
value={formData?.onFinish?.url}
|
|
||||||
onChange={(event) => {
|
|
||||||
setChanged(true);
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
onStop: {
|
|
||||||
...formData.onFinish,
|
|
||||||
url: event.target.value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Switch
|
|
||||||
colorScheme='green'
|
|
||||||
id='onFinishEnable'
|
|
||||||
value={formData?.onFinish?.enabled}
|
|
||||||
onChange={(event) => {
|
|
||||||
setChanged(true);
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
onStop: {
|
|
||||||
...formData.onStop,
|
|
||||||
enabled: event.target.value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</div>
|
|
||||||
<SubmitContainer
|
|
||||||
revert={revert}
|
|
||||||
submitting={submitting}
|
|
||||||
changed={changed}
|
|
||||||
status={status}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</ModalBody>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,14 +2,13 @@ import { createRef, Fragment, useCallback, useContext, useEffect } from 'react';
|
|||||||
import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd';
|
import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd';
|
||||||
import { Button } from '@chakra-ui/react';
|
import { Button } from '@chakra-ui/react';
|
||||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||||
import { useAtomValue } from 'jotai';
|
|
||||||
import { OntimeRundown, SupportedEvent } from 'ontime-types';
|
import { OntimeRundown, SupportedEvent } from 'ontime-types';
|
||||||
|
|
||||||
import { defaultPublicAtom, showQuickEntryAtom, startTimeIsLastEndAtom } from '../../common/atoms/LocalEventSettings';
|
|
||||||
import Empty from '../../common/components/state/Empty';
|
import Empty from '../../common/components/state/Empty';
|
||||||
import { CursorContext } from '../../common/context/CursorContext';
|
import { CursorContext } from '../../common/context/CursorContext';
|
||||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||||
import { useRundownEditor } from '../../common/hooks/useSocket';
|
import { useRundownEditor } from '../../common/hooks/useSocket';
|
||||||
|
import { useLocalEvent } from '../../common/stores/localEvent';
|
||||||
import { cloneEvent } from '../../common/utils/eventsManager';
|
import { cloneEvent } from '../../common/utils/eventsManager';
|
||||||
|
|
||||||
import QuickAddBlock from './quick-add-block/QuickAddBlock';
|
import QuickAddBlock from './quick-add-block/QuickAddBlock';
|
||||||
@@ -25,11 +24,13 @@ export default function Rundown(props: RundownProps) {
|
|||||||
const { entries } = props;
|
const { entries } = props;
|
||||||
const data = useRundownEditor();
|
const data = useRundownEditor();
|
||||||
const { cursor, moveCursorUp, moveCursorDown, moveCursorTo, isCursorLocked } = useContext(CursorContext);
|
const { cursor, moveCursorUp, moveCursorDown, moveCursorTo, isCursorLocked } = useContext(CursorContext);
|
||||||
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
|
|
||||||
const defaultPublic = useAtomValue(defaultPublicAtom);
|
|
||||||
const { addEvent, reorderEvent } = useEventAction();
|
const { addEvent, reorderEvent } = useEventAction();
|
||||||
const cursorRef = createRef<HTMLDivElement>();
|
const cursorRef = createRef<HTMLDivElement>();
|
||||||
const showQuickEntry = useAtomValue(showQuickEntryAtom);
|
|
||||||
|
const { eventSettings } = useLocalEvent();
|
||||||
|
const defaultPublic = eventSettings.defaultPublic;
|
||||||
|
const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd;
|
||||||
|
const showQuickEntry = eventSettings.showQuickEntry;
|
||||||
|
|
||||||
const insertAtCursor = useCallback(
|
const insertAtCursor = useCallback(
|
||||||
(type: SupportedEvent | 'clone', cursor: number) => {
|
(type: SupportedEvent | 'clone', cursor: number) => {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useCallback, useContext } from 'react';
|
import { useCallback, useContext } from 'react';
|
||||||
import { useAtom, useAtomValue } from 'jotai';
|
|
||||||
import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types';
|
import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types';
|
||||||
|
|
||||||
import { defaultPublicAtom, editorEventId, startTimeIsLastEndAtom } from '../../common/atoms/LocalEventSettings';
|
|
||||||
import { CursorContext } from '../../common/context/CursorContext';
|
import { CursorContext } from '../../common/context/CursorContext';
|
||||||
import { useEventAction } from '../../common/hooks/useEventAction';
|
import { useEventAction } from '../../common/hooks/useEventAction';
|
||||||
|
import { useEventEditorStore } from '../../common/stores/eventEditor';
|
||||||
|
import { useLocalEvent } from '../../common/stores/localEvent';
|
||||||
import { useEmitLog } from '../../common/stores/logger';
|
import { useEmitLog } from '../../common/stores/logger';
|
||||||
import { cloneEvent } from '../../common/utils/eventsManager';
|
import { cloneEvent } from '../../common/utils/eventsManager';
|
||||||
import { calculateDuration } from '../../common/utils/timesManager';
|
import { calculateDuration } from '../../common/utils/timesManager';
|
||||||
@@ -32,11 +32,13 @@ interface RundownEntryProps {
|
|||||||
export default function RundownEntry(props: RundownEntryProps) {
|
export default function RundownEntry(props: RundownEntryProps) {
|
||||||
const { index, eventIndex, data, selected, hasCursor, next, delay, previousEnd, previousEventId, playback } = props;
|
const { index, eventIndex, data, selected, hasCursor, next, delay, previousEnd, previousEventId, playback } = props;
|
||||||
const { emitError } = useEmitLog();
|
const { emitError } = useEmitLog();
|
||||||
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
|
const { openId, removeOpenEvent } = useEventEditorStore();
|
||||||
const defaultPublic = useAtomValue(defaultPublicAtom);
|
|
||||||
const { addEvent, updateEvent, deleteEvent } = useEventAction();
|
const { addEvent, updateEvent, deleteEvent } = useEventAction();
|
||||||
const { moveCursorTo } = useContext(CursorContext);
|
const { moveCursorTo } = useContext(CursorContext);
|
||||||
const [openId, setOpenId] = useAtom(editorEventId);
|
|
||||||
|
const { eventSettings } = useLocalEvent();
|
||||||
|
const defaultPublic = eventSettings.defaultPublic;
|
||||||
|
const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd;
|
||||||
|
|
||||||
// Create / delete new events
|
// Create / delete new events
|
||||||
type FieldValue = {
|
type FieldValue = {
|
||||||
@@ -71,7 +73,7 @@ export default function RundownEntry(props: RundownEntryProps) {
|
|||||||
}
|
}
|
||||||
case 'delete': {
|
case 'delete': {
|
||||||
if (openId === data.id) {
|
if (openId === data.id) {
|
||||||
setOpenId(null);
|
removeOpenEvent();
|
||||||
}
|
}
|
||||||
deleteEvent(data.id);
|
deleteEvent(data.id);
|
||||||
break;
|
break;
|
||||||
@@ -122,7 +124,7 @@ export default function RundownEntry(props: RundownEntryProps) {
|
|||||||
moveCursorTo,
|
moveCursorTo,
|
||||||
openId,
|
openId,
|
||||||
previousEventId,
|
previousEventId,
|
||||||
setOpenId,
|
removeOpenEvent,
|
||||||
startTimeIsLastEnd,
|
startTimeIsLastEnd,
|
||||||
updateEvent,
|
updateEvent,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -10,13 +10,12 @@ import { IoReload } from '@react-icons/all-files/io5/IoReload';
|
|||||||
import { IoRemoveCircle } from '@react-icons/all-files/io5/IoRemoveCircle';
|
import { IoRemoveCircle } from '@react-icons/all-files/io5/IoRemoveCircle';
|
||||||
import { IoRemoveCircleOutline } from '@react-icons/all-files/io5/IoRemoveCircleOutline';
|
import { IoRemoveCircleOutline } from '@react-icons/all-files/io5/IoRemoveCircleOutline';
|
||||||
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
|
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
|
||||||
import { useAtom } from 'jotai';
|
|
||||||
import { Playback } from 'ontime-types';
|
import { Playback } from 'ontime-types';
|
||||||
|
|
||||||
import { editorEventId } from '../../../common/atoms/LocalEventSettings';
|
|
||||||
import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn';
|
import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn';
|
||||||
import { useEventAction } from '../../../common/hooks/useEventAction';
|
import { useEventAction } from '../../../common/hooks/useEventAction';
|
||||||
import { setEventPlayback } from '../../../common/hooks/useSocket';
|
import { setEventPlayback } from '../../../common/hooks/useSocket';
|
||||||
|
import { useEventEditorStore } from '../../../common/stores/eventEditor';
|
||||||
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
|
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
|
||||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||||
import { EventItemActions } from '../RundownEntry';
|
import { EventItemActions } from '../RundownEntry';
|
||||||
@@ -78,7 +77,7 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
actionHandler,
|
actionHandler,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [openId, setOpenId] = useAtom(editorEventId);
|
const { openId, setOpenEvent, removeOpenEvent } = useEventEditorStore();
|
||||||
const { updateEvent } = useEventAction();
|
const { updateEvent } = useEventAction();
|
||||||
const [blockTitle, setBlockTitle] = useState<string>(title || '');
|
const [blockTitle, setBlockTitle] = useState<string>(title || '');
|
||||||
const onFocusRef = useRef<null | HTMLSpanElement>(null);
|
const onFocusRef = useRef<null | HTMLSpanElement>(null);
|
||||||
@@ -111,6 +110,14 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
[title, updateEvent, eventId],
|
[title, updateEvent, eventId],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const toggleOpenEvent = useCallback(() => {
|
||||||
|
if (openId === eventId) {
|
||||||
|
removeOpenEvent();
|
||||||
|
} else {
|
||||||
|
setOpenEvent(eventId);
|
||||||
|
}
|
||||||
|
}, [eventId, openId, removeOpenEvent, setOpenEvent]);
|
||||||
|
|
||||||
const eventIsPlaying = selected && playback === Playback.Play;
|
const eventIsPlaying = selected && playback === Playback.Play;
|
||||||
const playBtnStyles = { _hover: {} };
|
const playBtnStyles = { _hover: {} };
|
||||||
if (!skip && eventIsPlaying) {
|
if (!skip && eventIsPlaying) {
|
||||||
@@ -221,7 +228,7 @@ export default function EventBlock(props: EventBlockProps) {
|
|||||||
variant='ontime-subtle-white'
|
variant='ontime-subtle-white'
|
||||||
size='sm'
|
size='sm'
|
||||||
icon={<IoOptions />}
|
icon={<IoOptions />}
|
||||||
clickHandler={() => setOpenId((prev) => (prev === eventId ? null : eventId))}
|
clickHandler={toggleOpenEvent}
|
||||||
tooltip='Event options'
|
tooltip='Event options'
|
||||||
aria-label='Event options'
|
aria-label='Event options'
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
|
|||||||
@@ -11,25 +11,16 @@ interface EventBlockProgressBarProps {
|
|||||||
|
|
||||||
export default function EventBlockProgressBar(props: EventBlockProgressBarProps) {
|
export default function EventBlockProgressBar(props: EventBlockProgressBarProps) {
|
||||||
const { playback } = props;
|
const { playback } = props;
|
||||||
const { data: timer } = useTimer();
|
const timer = useTimer();
|
||||||
|
|
||||||
const now = Math.floor(Math.max((timer?.current ?? 1) / 1000, 0));
|
const now = Math.floor(Math.max((timer?.current ?? 1) / 1000, 0));
|
||||||
const complete = (timer?.duration ?? 1) / 1000;
|
const complete = (timer?.duration ?? 1) / 1000;
|
||||||
const elapsed = clamp(100 - (now * 100) / complete, 0, 100);
|
const elapsed = clamp(100 - (now * 100) / complete, 0, 100);
|
||||||
const progress = `${elapsed}%`;
|
const progress = `${elapsed}%`;
|
||||||
|
|
||||||
if ((timer?.current ?? 0) < 0) {
|
if ((timer?.current ?? 0) < 0) {
|
||||||
return (
|
return <div className={`${style.progressBar} ${style.overtime}`} style={{ width: '100%' }} />;
|
||||||
<div
|
|
||||||
className={`${style.progressBar} ${style.overtime}`}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <div className={`${style.progressBar} ${playback ? style[playback] : ''}`} style={{ width: progress }} />;
|
||||||
<div
|
|
||||||
className={`${style.progressBar} ${playback ? style[playback] : ''}`}
|
|
||||||
style={{ width: progress }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
import { Button, Checkbox, Tooltip } from '@chakra-ui/react';
|
import { Button, Checkbox, Tooltip } from '@chakra-ui/react';
|
||||||
import { useAtomValue } from 'jotai';
|
|
||||||
import { SupportedEvent } from 'ontime-types';
|
import { SupportedEvent } from 'ontime-types';
|
||||||
|
|
||||||
import { defaultPublicAtom, startTimeIsLastEndAtom } from '../../../common/atoms/LocalEventSettings';
|
|
||||||
import { useEventAction } from '../../../common/hooks/useEventAction';
|
import { useEventAction } from '../../../common/hooks/useEventAction';
|
||||||
|
import { useLocalEvent } from '../../../common/stores/localEvent';
|
||||||
import { useEmitLog } from '../../../common/stores/logger';
|
import { useEmitLog } from '../../../common/stores/logger';
|
||||||
import { tooltipDelayMid } from '../../../ontimeConfig';
|
import { tooltipDelayMid } from '../../../ontimeConfig';
|
||||||
|
|
||||||
@@ -22,11 +21,14 @@ export default function QuickAddBlock(props: QuickAddBlockProps) {
|
|||||||
const { showKbd, eventId, previousEventId, disableAddDelay = true, disableAddBlock } = props;
|
const { showKbd, eventId, previousEventId, disableAddDelay = true, disableAddBlock } = props;
|
||||||
const { addEvent } = useEventAction();
|
const { addEvent } = useEventAction();
|
||||||
const { emitError } = useEmitLog();
|
const { emitError } = useEmitLog();
|
||||||
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
|
|
||||||
const defaultPublic = useAtomValue(defaultPublicAtom);
|
|
||||||
const doStartTime = useRef<HTMLInputElement | null>(null);
|
const doStartTime = useRef<HTMLInputElement | null>(null);
|
||||||
const doPublic = useRef<HTMLInputElement | null>(null);
|
const doPublic = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
const { eventSettings } = useLocalEvent();
|
||||||
|
const defaultPublic = eventSettings.defaultPublic;
|
||||||
|
const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd;
|
||||||
|
|
||||||
const handleCreateEvent = useCallback(
|
const handleCreateEvent = useCallback(
|
||||||
(eventType: SupportedEvent) => {
|
(eventType: SupportedEvent) => {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
|
|||||||
@@ -20,17 +20,16 @@ import PlaybackIcon from './tableElements/PlaybackIcon';
|
|||||||
import style from './Table.module.scss';
|
import style from './Table.module.scss';
|
||||||
|
|
||||||
export default function TableHeader({ handleCSVExport, featureData }) {
|
export default function TableHeader({ handleCSVExport, featureData }) {
|
||||||
const { followSelected, showSettings, toggleTheme, toggleSettings, toggleFollow } =
|
const { followSelected, showSettings, toggleTheme, toggleSettings, toggleFollow } = useContext(TableSettingsContext);
|
||||||
useContext(TableSettingsContext);
|
const timer = useTimer();
|
||||||
const { data: timer } = useTimer();
|
|
||||||
const { isFullScreen, toggleFullScreen } = useFullscreen();
|
const { isFullScreen, toggleFullScreen } = useFullscreen();
|
||||||
const { data: event } = useEventData();
|
const { data: event } = useEventData();
|
||||||
|
|
||||||
const selected = !featureData.numEvents
|
const selected = !featureData.numEvents
|
||||||
? 'No events'
|
? 'No events'
|
||||||
: `Event ${featureData.selectedEventIndex != null ? featureData.selectedEventIndex + 1 : '-'}/${
|
: `Event ${featureData.selectedEventIndex != null ? featureData.selectedEventIndex + 1 : '-'}/${
|
||||||
featureData.numEvents ? featureData.numEvents : '-'
|
featureData.numEvents ? featureData.numEvents : '-'
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
// prepare presentation variables
|
// prepare presentation variables
|
||||||
const isOvertime = timer.current < 0;
|
const isOvertime = timer.current < 0;
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import style from './Table.module.scss';
|
|||||||
export default function TableWrapper() {
|
export default function TableWrapper() {
|
||||||
const { data: rundown } = useRundown();
|
const { data: rundown } = useRundown();
|
||||||
const { data: userFields } = useUserFields();
|
const { data: userFields } = useUserFields();
|
||||||
const { data: featureData } = useCuesheet();
|
|
||||||
const { updateEvent } = useEventAction();
|
const { updateEvent } = useEventAction();
|
||||||
|
const featureData = useCuesheet();
|
||||||
const { theme } = useContext(TableSettingsContext);
|
const { theme } = useContext(TableSettingsContext);
|
||||||
|
|
||||||
// Set window title
|
// Set window title
|
||||||
@@ -30,11 +30,11 @@ export default function TableWrapper() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if value is the same
|
// check if value is the same
|
||||||
const event = rundown[rowIndex];
|
const event = rundown[rowIndex];
|
||||||
if (event == null) {
|
if (event == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event[accessor] === payload) {
|
if (event[accessor] === payload) {
|
||||||
return;
|
return;
|
||||||
@@ -52,13 +52,15 @@ export default function TableWrapper() {
|
|||||||
[accessor]: cleanVal,
|
[accessor]: cleanVal,
|
||||||
};
|
};
|
||||||
|
|
||||||
// submit
|
// submit
|
||||||
try {
|
try {
|
||||||
await updateEvent(mutationObject);
|
await updateEvent(mutationObject);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}, [updateEvent, rundown]);
|
},
|
||||||
|
[updateEvent, rundown],
|
||||||
|
);
|
||||||
|
|
||||||
const exportHandler = useCallback(
|
const exportHandler = useCallback(
|
||||||
(headerData) => {
|
(headerData) => {
|
||||||
@@ -75,17 +77,14 @@ export default function TableWrapper() {
|
|||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
},
|
},
|
||||||
[rundown, userFields]
|
[rundown, userFields],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (typeof rundown === 'undefined' || typeof userFields === 'undefined') {
|
if (typeof rundown === 'undefined' || typeof userFields === 'undefined') {
|
||||||
return <span>loading...</span>;
|
return <span>loading...</span>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={theme === 'dark' ? style.tableWrapper__dark : style.tableWrapper} data-testid='cuesheet'>
|
||||||
className={theme === 'dark' ? style.tableWrapper__dark : style.tableWrapper}
|
|
||||||
data-testid="cuesheet"
|
|
||||||
>
|
|
||||||
<TableHeader handleCSVExport={exportHandler} featureData={featureData} />
|
<TableHeader handleCSVExport={exportHandler} featureData={featureData} />
|
||||||
<OntimeTable
|
<OntimeTable
|
||||||
tableData={rundown}
|
tableData={rundown}
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import { ReactNode, useMemo } from 'react';
|
import { ReactNode, useMemo } from 'react';
|
||||||
import { Playback } from 'ontime-types';
|
import { Playback, TitleBlock } from 'ontime-types';
|
||||||
|
|
||||||
import useEventData from '../../common/hooks-query/useEventData';
|
import useEventData from '../../common/hooks-query/useEventData';
|
||||||
import useRundown from '../../common/hooks-query/useRundown';
|
import useRundown from '../../common/hooks-query/useRundown';
|
||||||
import useViewSettings from '../../common/hooks-query/useViewSettings';
|
import useViewSettings from '../../common/hooks-query/useViewSettings';
|
||||||
import { useRuntimeStore } from '../../common/stores/runtime';
|
import { useRuntimeStore } from '../../common/stores/runtime';
|
||||||
|
import { useViewOptionsStore } from '../../common/stores/viewOptions';
|
||||||
|
|
||||||
|
export type TitleManager = TitleBlock & { showNow: boolean; showNext: boolean };
|
||||||
|
|
||||||
const withData = (Component: ReactNode) => {
|
const withData = (Component: ReactNode) => {
|
||||||
return (props) => {
|
return (props) => {
|
||||||
|
// persisted app state
|
||||||
|
const { mirror: isMirrored } = useViewOptionsStore();
|
||||||
|
|
||||||
// HTTP API data
|
// HTTP API data
|
||||||
const { data: eventsData } = useRundown();
|
const { data: eventsData } = useRundown();
|
||||||
@@ -41,7 +46,7 @@ const withData = (Component: ReactNode) => {
|
|||||||
let showNext = true;
|
let showNext = true;
|
||||||
if (!titles.titleNext && !titles.subtitleNext && !titles.presenterNext) showNext = false;
|
if (!titles.titleNext && !titles.subtitleNext && !titles.presenterNext) showNext = false;
|
||||||
|
|
||||||
const titleManager = { ...titles, showNow: showNow, showNext: showNext };
|
const titleManager: TitleManager = { ...titles, showNow: showNow, showNext: showNext };
|
||||||
|
|
||||||
/********************************************/
|
/********************************************/
|
||||||
/*** + publicTitleManager ***/
|
/*** + publicTitleManager ***/
|
||||||
@@ -56,7 +61,7 @@ const withData = (Component: ReactNode) => {
|
|||||||
let showPublicNext = true;
|
let showPublicNext = true;
|
||||||
if (!titlesPublic.titleNext && !titlesPublic.subtitleNext && !titlesPublic.presenterNext) showPublicNext = false;
|
if (!titlesPublic.titleNext && !titlesPublic.subtitleNext && !titlesPublic.presenterNext) showPublicNext = false;
|
||||||
|
|
||||||
const publicTitleManager = {
|
const publicTitleManager: TitleManager = {
|
||||||
...titlesPublic,
|
...titlesPublic,
|
||||||
showNow: showPublicNow,
|
showNow: showPublicNow,
|
||||||
showNext: showPublicNext,
|
showNext: showPublicNext,
|
||||||
@@ -85,6 +90,7 @@ const withData = (Component: ReactNode) => {
|
|||||||
return (
|
return (
|
||||||
<Component
|
<Component
|
||||||
{...props}
|
{...props}
|
||||||
|
isMirrored={isMirrored}
|
||||||
pres={timerMessage}
|
pres={timerMessage}
|
||||||
publ={publicMessage}
|
publ={publicMessage}
|
||||||
lower={lowerMessage}
|
lower={lowerMessage}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
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 { useAtom } from 'jotai';
|
import { EventData, Message, OntimeEvent, ViewSettings } from 'ontime-types';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
import ProgressBar from '../../../common/components/progress-bar/ProgressBar';
|
import ProgressBar from '../../../common/components/progress-bar/ProgressBar';
|
||||||
import Schedule from '../../../common/components/schedule/Schedule';
|
import Schedule from '../../../common/components/schedule/Schedule';
|
||||||
@@ -13,10 +11,12 @@ import { ScheduleProvider } from '../../../common/components/schedule/ScheduleCo
|
|||||||
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 { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
|
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||||
import { formatDisplay, millisToSeconds } from '../../../common/utils/dateConfig';
|
import { formatDisplay, millisToSeconds } from '../../../common/utils/dateConfig';
|
||||||
import { getEventsWithDelay } from '../../../common/utils/eventsManager';
|
import { getEventsWithDelay } from '../../../common/utils/eventsManager';
|
||||||
import { formatTime } from '../../../common/utils/time';
|
import { formatTime } from '../../../common/utils/time';
|
||||||
import { titleVariants } from '../common/animation';
|
import { titleVariants } from '../common/animation';
|
||||||
|
import { TitleManager } from '../ViewWrapper';
|
||||||
|
|
||||||
import './Backstage.scss';
|
import './Backstage.scss';
|
||||||
|
|
||||||
@@ -25,21 +25,20 @@ const formatOptions = {
|
|||||||
format: 'hh:mm:ss a',
|
format: 'hh:mm:ss a',
|
||||||
};
|
};
|
||||||
|
|
||||||
Backstage.propTypes = {
|
interface BackstageProps {
|
||||||
publ: PropTypes.object,
|
isMirrored: boolean;
|
||||||
title: PropTypes.object,
|
publ: Message;
|
||||||
time: PropTypes.object,
|
title: TitleManager;
|
||||||
backstageEvents: PropTypes.array,
|
time: TimeManagerType;
|
||||||
selectedId: PropTypes.string,
|
backstageEvents: OntimeEvent[];
|
||||||
general: PropTypes.object,
|
selectedId: string | null;
|
||||||
viewSettings: PropTypes.object,
|
general: EventData;
|
||||||
};
|
viewSettings: ViewSettings;
|
||||||
|
}
|
||||||
|
|
||||||
// @ts-expect-error unable to type just yet
|
export default function Backstage(props: BackstageProps) {
|
||||||
export default function Backstage(props) {
|
const { isMirrored, publ, title, time, backstageEvents, selectedId, general, viewSettings } = props;
|
||||||
const { publ, title, time, backstageEvents, selectedId, general, viewSettings } = props;
|
|
||||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||||
const [isMirrored] = useAtom(mirrorViewersAtom);
|
|
||||||
|
|
||||||
// Set window title
|
// Set window title
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { useAtom } from 'jotai';
|
|
||||||
import { ViewSettings } from 'ontime-types';
|
import { ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
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';
|
||||||
@@ -14,6 +12,7 @@ import { formatTime } from '../../../common/utils/time';
|
|||||||
import './Clock.scss';
|
import './Clock.scss';
|
||||||
|
|
||||||
interface ClockProps {
|
interface ClockProps {
|
||||||
|
isMirrored: boolean;
|
||||||
time: TimeManagerType;
|
time: TimeManagerType;
|
||||||
viewSettings: ViewSettings;
|
viewSettings: ViewSettings;
|
||||||
}
|
}
|
||||||
@@ -24,10 +23,9 @@ const formatOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function Clock(props: ClockProps) {
|
export default function Clock(props: ClockProps) {
|
||||||
const { time, viewSettings } = props;
|
const { isMirrored, time, viewSettings } = props;
|
||||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const [isMirrored] = useAtom(mirrorViewersAtom);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = 'ontime - Clock';
|
document.title = 'ontime - Clock';
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { useAtom } from 'jotai';
|
import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent, ViewSettings } from 'ontime-types';
|
||||||
import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
|
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||||
import { formatDisplay, millisToSeconds } from '../../../common/utils/dateConfig';
|
import { formatDisplay, millisToSeconds } from '../../../common/utils/dateConfig';
|
||||||
import getDelayTo from '../../../common/utils/getDelayTo';
|
import getDelayTo from '../../../common/utils/getDelayTo';
|
||||||
import { formatTime } from '../../../common/utils/time';
|
import { formatTime } from '../../../common/utils/time';
|
||||||
@@ -27,19 +25,18 @@ const formatOptionsFinished = {
|
|||||||
format: 'hh:mm a',
|
format: 'hh:mm a',
|
||||||
};
|
};
|
||||||
|
|
||||||
Countdown.propTypes = {
|
interface CountdownProps {
|
||||||
backstageEvents: PropTypes.array,
|
isMirrored: boolean;
|
||||||
time: PropTypes.object,
|
backstageEvents: OntimeEvent[];
|
||||||
selectedId: PropTypes.string,
|
time: TimeManagerType;
|
||||||
viewSettings: PropTypes.object,
|
selectedId: string | null;
|
||||||
};
|
viewSettings: ViewSettings;
|
||||||
|
}
|
||||||
|
|
||||||
// @ts-expect-error we are unable to type this just yet
|
export default function Countdown(props: CountdownProps) {
|
||||||
export default function Countdown(props) {
|
const { isMirrored, backstageEvents, time, selectedId, viewSettings } = props;
|
||||||
const { backstageEvents, time, selectedId, viewSettings } = props;
|
|
||||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const [isMirrored] = useAtom(mirrorViewersAtom);
|
|
||||||
|
|
||||||
const [follow, setFollow] = useState<OntimeEvent | null>(null);
|
const [follow, setFollow] = useState<OntimeEvent | null>(null);
|
||||||
const [runningTimer, setRunningTimer] = useState(0);
|
const [runningTimer, setRunningTimer] = useState(0);
|
||||||
@@ -97,20 +94,15 @@ export default function Countdown(props) {
|
|||||||
const delayedTimerStyles = delay > 0 ? 'aux-timers__value--delayed' : '';
|
const delayedTimerStyles = delay > 0 ? 'aux-timers__value--delayed' : '';
|
||||||
|
|
||||||
const clock = formatTime(time.clock, formatOptions);
|
const clock = formatTime(time.clock, formatOptions);
|
||||||
const startTime =
|
const startTime = follow === null ? '...' : formatTime(follow.timeStart + delay, formatOptions);
|
||||||
follow === null
|
const endTime = follow === null ? '...' : formatTime(follow.timeEnd + delay, formatOptions);
|
||||||
? '...'
|
const formattedTimer =
|
||||||
: formatTime(follow.timeStart + delay, formatOptions);
|
runningMessage === TimerMessage.ended
|
||||||
const endTime =
|
? formatTime(runningTimer, formatOptionsFinished)
|
||||||
follow === null
|
: formatDisplay(
|
||||||
? '...'
|
isSelected ? millisToSeconds(runningTimer) : millisToSeconds(runningTimer + delay),
|
||||||
: formatTime(follow.timeEnd + delay, formatOptions);
|
isSelected || time.waiting,
|
||||||
const formattedTimer = runningMessage === TimerMessage.ended
|
);
|
||||||
? formatTime(runningTimer, formatOptionsFinished)
|
|
||||||
: formatDisplay(
|
|
||||||
isSelected ? millisToSeconds(runningTimer) : millisToSeconds(runningTimer + delay),
|
|
||||||
isSelected || time.waiting,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`countdown ${isMirrored ? 'mirror' : ''}`} data-testid='countdown-view'>
|
<div className={`countdown ${isMirrored ? 'mirror' : ''}`} data-testid='countdown-view'>
|
||||||
@@ -119,7 +111,6 @@ export default function Countdown(props) {
|
|||||||
<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'>Time Now</div>
|
<div className='label'>Time Now</div>
|
||||||
<div className='time'>{clock}</div>
|
<div className='time'>{clock}</div>
|
||||||
@@ -127,11 +118,7 @@ export default function Countdown(props) {
|
|||||||
|
|
||||||
<div className='status'>{runningMessage}</div>
|
<div className='status'>{runningMessage}</div>
|
||||||
|
|
||||||
<span
|
<span className={`timer ${standby ? 'timer--paused' : ''} ${isRunningFinished ? 'timer--finished' : ''}`}>
|
||||||
className={`timer ${standby ? 'timer--paused' : ''} ${
|
|
||||||
isRunningFinished ? 'timer--finished' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{formattedTimer}
|
{formattedTimer}
|
||||||
</span>
|
</span>
|
||||||
<div className='title'>{follow?.title || 'Untitled Event'}</div>
|
<div className='title'>{follow?.title || 'Untitled Event'}</div>
|
||||||
@@ -139,18 +126,13 @@ export default function Countdown(props) {
|
|||||||
<div className='timer-group'>
|
<div className='timer-group'>
|
||||||
<div className='aux-timers'>
|
<div className='aux-timers'>
|
||||||
<div className='aux-timers__label'>Start Time</div>
|
<div className='aux-timers__label'>Start Time</div>
|
||||||
<span className={`aux-timers__value ${delayedTimerStyles}`}>
|
<span className={`aux-timers__value ${delayedTimerStyles}`}>{startTime}</span>
|
||||||
{startTime}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className='aux-timers'>
|
<div className='aux-timers'>
|
||||||
<div className='aux-timers__label'>End Time</div>
|
<div className='aux-timers__label'>End Time</div>
|
||||||
<span className={`aux-timers__value ${delayedTimerStyles}`}>
|
<span className={`aux-timers__value ${delayedTimerStyles}`}>{endTime}</span>
|
||||||
{endTime}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { useAtom } from 'jotai';
|
|
||||||
import { EventData, Message, Playback, TimerType, ViewSettings } from 'ontime-types';
|
import { EventData, Message, Playback, TimerType, ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
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';
|
||||||
@@ -14,6 +12,7 @@ import { formatTimerDisplay, getTimerByType } from '../common/viewerUtils';
|
|||||||
import './MinimalTimer.scss';
|
import './MinimalTimer.scss';
|
||||||
|
|
||||||
interface MinimalTimerProps {
|
interface MinimalTimerProps {
|
||||||
|
isMirrored: boolean;
|
||||||
pres: Message;
|
pres: Message;
|
||||||
time: TimeManagerType;
|
time: TimeManagerType;
|
||||||
viewSettings: ViewSettings;
|
viewSettings: ViewSettings;
|
||||||
@@ -21,10 +20,9 @@ interface MinimalTimerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function MinimalTimer(props: MinimalTimerProps) {
|
export default function MinimalTimer(props: MinimalTimerProps) {
|
||||||
const { pres, time, viewSettings, general } = props;
|
const { isMirrored, pres, time, viewSettings, general } = props;
|
||||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const [isMirrored] = useAtom(mirrorViewersAtom);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = 'ontime - Minimal Timer';
|
document.title = 'ontime - Minimal Timer';
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
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 { useAtom } from 'jotai';
|
import { EventData, Message, OntimeEvent, ViewSettings } from 'ontime-types';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
import Schedule from '../../../common/components/schedule/Schedule';
|
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 { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
|
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||||
import { formatTime } from '../../../common/utils/time';
|
import { formatTime } from '../../../common/utils/time';
|
||||||
import { titleVariants } from '../common/animation';
|
import { titleVariants } from '../common/animation';
|
||||||
|
import { TitleManager } from '../ViewWrapper';
|
||||||
|
|
||||||
import './Public.scss';
|
import './Public.scss';
|
||||||
|
|
||||||
@@ -22,21 +22,20 @@ const formatOptions = {
|
|||||||
format: 'hh:mm:ss a',
|
format: 'hh:mm:ss a',
|
||||||
};
|
};
|
||||||
|
|
||||||
Public.propTypes = {
|
interface BackstageProps {
|
||||||
publ: PropTypes.object,
|
isMirrored: boolean;
|
||||||
publicTitle: PropTypes.object,
|
publ: Message;
|
||||||
time: PropTypes.object,
|
publicTitle: TitleManager;
|
||||||
events: PropTypes.array,
|
time: TimeManagerType;
|
||||||
publicSelectedId: PropTypes.string,
|
events: OntimeEvent[];
|
||||||
general: PropTypes.object,
|
publicSelectedId: string | null;
|
||||||
viewSettings: PropTypes.object,
|
general: EventData;
|
||||||
};
|
viewSettings: ViewSettings;
|
||||||
|
}
|
||||||
|
|
||||||
// @ts-expect-error unable to type just yet
|
export default function Public(props: BackstageProps) {
|
||||||
export default function Public(props) {
|
const { isMirrored, publ, publicTitle, time, events, publicSelectedId, general, viewSettings } = props;
|
||||||
const { publ, publicTitle, time, events, publicSelectedId, general, viewSettings } = props;
|
|
||||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||||
const [isMirrored] = useAtom(mirrorViewersAtom);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = 'ontime - Public Screen';
|
document.title = 'ontime - Public Screen';
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { useAtom } from 'jotai';
|
import { millisToString } from 'ontime-utils';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
import useFitText from '../../../common/hooks/useFitText';
|
import useFitText from '../../../common/hooks/useFitText';
|
||||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
@@ -13,7 +12,6 @@ import { formatEventList, getEventsWithDelay, trimEventlist } from '../../../com
|
|||||||
import { formatTime } from '../../../common/utils/time';
|
import { formatTime } from '../../../common/utils/time';
|
||||||
|
|
||||||
import './StudioClock.scss';
|
import './StudioClock.scss';
|
||||||
import { millisToString } from 'ontime-utils';
|
|
||||||
|
|
||||||
const formatOptions = {
|
const formatOptions = {
|
||||||
showSeconds: false,
|
showSeconds: false,
|
||||||
@@ -21,6 +19,7 @@ const formatOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
StudioClock.propTypes = {
|
StudioClock.propTypes = {
|
||||||
|
isMirrored: PropTypes.bool,
|
||||||
title: PropTypes.object,
|
title: PropTypes.object,
|
||||||
time: PropTypes.object,
|
time: PropTypes.object,
|
||||||
backstageEvents: PropTypes.array,
|
backstageEvents: PropTypes.array,
|
||||||
@@ -31,14 +30,13 @@ StudioClock.propTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function StudioClock(props) {
|
export default function StudioClock(props) {
|
||||||
const { title, time, backstageEvents, selectedId, nextId, onAir, viewSettings } = props;
|
const { isMirrored, title, time, backstageEvents, selectedId, nextId, onAir, viewSettings } = 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);
|
||||||
const { fontSize: titleFontSize, ref: titleRef } = useFitText({ maxFontSize: 500 });
|
const { fontSize: titleFontSize, ref: titleRef } = useFitText({ maxFontSize: 500 });
|
||||||
|
|
||||||
const [schedule, setSchedule] = useState([]);
|
const [schedule, setSchedule] = useState([]);
|
||||||
const [isMirrored] = useAtom(mirrorViewersAtom);
|
|
||||||
|
|
||||||
const activeIndicators = [...Array(12).keys()];
|
const activeIndicators = [...Array(12).keys()];
|
||||||
const secondsIndicators = [...Array(60).keys()];
|
const secondsIndicators = [...Array(60).keys()];
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { useAtom } from 'jotai';
|
import { EventData, Message, Playback, TimerType, ViewSettings } from 'ontime-types';
|
||||||
import { Playback, TimerType } from 'ontime-types';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
||||||
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
|
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
import ProgressBar from '../../../common/components/progress-bar/ProgressBar';
|
import ProgressBar from '../../../common/components/progress-bar/ProgressBar';
|
||||||
import TitleCard from '../../../common/components/title-card/TitleCard';
|
import TitleCard from '../../../common/components/title-card/TitleCard';
|
||||||
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
|
||||||
|
import { TimeManagerType } from '../../../common/models/TimeManager.type';
|
||||||
import { formatTime } from '../../../common/utils/time';
|
import { formatTime } from '../../../common/utils/time';
|
||||||
import { formatTimerDisplay, getTimerByType } from '../common/viewerUtils';
|
import { formatTimerDisplay, getTimerByType } from '../common/viewerUtils';
|
||||||
|
import { TitleManager } from '../ViewWrapper';
|
||||||
|
|
||||||
import './Timer.scss';
|
import './Timer.scss';
|
||||||
|
|
||||||
@@ -36,19 +35,18 @@ const titleVariants = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Timer.propTypes = {
|
interface TimerProps {
|
||||||
general: PropTypes.object,
|
isMirrored: boolean;
|
||||||
pres: PropTypes.object,
|
general: EventData;
|
||||||
title: PropTypes.object,
|
pres: Message;
|
||||||
time: PropTypes.object,
|
title: TitleManager;
|
||||||
viewSettings: PropTypes.object,
|
time: TimeManagerType;
|
||||||
};
|
viewSettings: ViewSettings;
|
||||||
|
}
|
||||||
|
|
||||||
// @ts-expect-error unable to type just yet
|
export default function Timer(props: TimerProps) {
|
||||||
export default function Timer(props) {
|
const { isMirrored, general, pres, title, time, viewSettings } = props;
|
||||||
const { general, pres, title, time, viewSettings } = props;
|
|
||||||
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
|
||||||
const [isMirrored] = useAtom(mirrorViewersAtom);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = 'ontime - Timer';
|
document.title = 'ontime - Timer';
|
||||||
|
|||||||
+13
-33
@@ -1,14 +1,4 @@
|
|||||||
const {
|
const { app, BrowserWindow, Menu, globalShortcut, Tray, dialog, ipcMain, shell, Notification } = require('electron');
|
||||||
app,
|
|
||||||
BrowserWindow,
|
|
||||||
Menu,
|
|
||||||
globalShortcut,
|
|
||||||
Tray,
|
|
||||||
dialog,
|
|
||||||
ipcMain,
|
|
||||||
shell,
|
|
||||||
Notification,
|
|
||||||
} = require('electron');
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const electronConfig = require('./electron.config');
|
const electronConfig = require('./electron.config');
|
||||||
|
|
||||||
@@ -40,14 +30,13 @@ let splash;
|
|||||||
let tray = null;
|
let tray = null;
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
|
|
||||||
// in dev mode, we expect both UI and server to be running
|
// in dev mode, we expect both UI and server to be running
|
||||||
if (!isProduction) {
|
if (!isProduction) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ontimeServer = require(nodePath)
|
const ontimeServer = require(nodePath);
|
||||||
const { startDb, startServer, startOSCServer, startIntegrations } = ontimeServer;
|
const { startDb, startServer, startOSCServer, startIntegrations } = ontimeServer;
|
||||||
|
|
||||||
await startDb();
|
await startDb();
|
||||||
@@ -76,9 +65,7 @@ function showNotification(title, text) {
|
|||||||
function appShutdown() {
|
function appShutdown() {
|
||||||
// terminate node service
|
// terminate node service
|
||||||
(async () => {
|
(async () => {
|
||||||
console.log('asking for shutdown 1', nodePath)
|
const ontimeServer = require(nodePath);
|
||||||
const ontimeServer = require(nodePath)
|
|
||||||
console.log('asking for shutdown 2', ontimeServer)
|
|
||||||
const { shutdown } = ontimeServer;
|
const { shutdown } = ontimeServer;
|
||||||
await shutdown(electronConfig.appIni.shutdownCode);
|
await shutdown(electronConfig.appIni.shutdownCode);
|
||||||
})();
|
})();
|
||||||
@@ -129,7 +116,8 @@ function createWindow() {
|
|||||||
skipTaskbar: true,
|
skipTaskbar: true,
|
||||||
});
|
});
|
||||||
splash.setIgnoreMouseEvents(true);
|
splash.setIgnoreMouseEvents(true);
|
||||||
splash.loadURL(`file://${__dirname}/src/splash/splash.html`);
|
const splashPath = path.join('file://', __dirname, '/src/splash/splash.html');
|
||||||
|
splash.loadURL(splashPath);
|
||||||
|
|
||||||
win = new BrowserWindow({
|
win = new BrowserWindow({
|
||||||
width: 1920,
|
width: 1920,
|
||||||
@@ -167,15 +155,13 @@ app.whenReady().then(() => {
|
|||||||
// (available regardless of whether app is in focus)
|
// (available regardless of whether app is in focus)
|
||||||
// bring focus to window
|
// bring focus to window
|
||||||
globalShortcut.register('Alt+1', () => {
|
globalShortcut.register('Alt+1', () => {
|
||||||
bringToFront();
|
bringToFront();
|
||||||
});
|
});
|
||||||
|
|
||||||
// cheat to schedule process
|
// cheat to schedule process
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Load page served by node or use React dev run
|
// Load page served by node or use React dev run
|
||||||
const clientUrl = isProduction
|
const clientUrl = isProduction ? electronConfig.reactAppUrl.production : electronConfig.reactAppUrl.development;
|
||||||
? electronConfig.reactAppUrl.production
|
|
||||||
: electronConfig.reactAppUrl.development;
|
|
||||||
|
|
||||||
win.loadURL(clientUrl).then(() => {
|
win.loadURL(clientUrl).then(() => {
|
||||||
win.webContents.setBackgroundThrottling(false);
|
win.webContents.setBackgroundThrottling(false);
|
||||||
@@ -212,13 +198,13 @@ app.whenReady().then(() => {
|
|||||||
|
|
||||||
// Define context menu
|
// Define context menu
|
||||||
const { getTrayMenu } = require('./src/menu/trayMenu.js');
|
const { getTrayMenu } = require('./src/menu/trayMenu.js');
|
||||||
const trayMenuTemplate = getTrayMenu(bringToFront, askToQuit)
|
const trayMenuTemplate = getTrayMenu(bringToFront, askToQuit);
|
||||||
const trayContextMenu = Menu.buildFromTemplate(trayMenuTemplate);
|
const trayContextMenu = Menu.buildFromTemplate(trayMenuTemplate);
|
||||||
tray.setContextMenu(trayContextMenu);
|
tray.setContextMenu(trayContextMenu);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { getApplicationMenu } = require('./src/menu/applicationMenu.js');
|
const { getApplicationMenu } = require('./src/menu/applicationMenu.js');
|
||||||
const template = getApplicationMenu(isMac, askToQuit)
|
const template = getApplicationMenu(isMac, askToQuit);
|
||||||
const menu = Menu.buildFromTemplate(template);
|
const menu = Menu.buildFromTemplate(template);
|
||||||
Menu.setApplicationMenu(menu);
|
Menu.setApplicationMenu(menu);
|
||||||
|
|
||||||
@@ -227,16 +213,10 @@ app.once('will-quit', () => {
|
|||||||
globalShortcut.unregisterAll();
|
globalShortcut.unregisterAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get messages from react
|
|
||||||
// Test message
|
|
||||||
ipcMain.on('test-message', (event, arg) => {
|
|
||||||
showNotification('Test Message', 'test from react', arg);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ask for main window reload
|
// Ask for main window reload
|
||||||
// Test message
|
// Test message
|
||||||
ipcMain.on('reload', () => {
|
ipcMain.on('reload', () => {
|
||||||
win?.reload();
|
win?.reload();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Terminate
|
// Terminate
|
||||||
@@ -258,7 +238,7 @@ ipcMain.on('set-window', (event, arg) => {
|
|||||||
win.webContents.openDevTools({ mode: 'detach' });
|
win.webContents.openDevTools({ mode: 'detach' });
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.log('Electron unhandled window request', arg)
|
console.log('Electron unhandled window request', arg);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Generated
-45
@@ -70,7 +70,6 @@ importers:
|
|||||||
eslint-plugin-simple-import-sort: ^8.0.0
|
eslint-plugin-simple-import-sort: ^8.0.0
|
||||||
eslint-plugin-testing-library: ^5.9.1
|
eslint-plugin-testing-library: ^5.9.1
|
||||||
framer-motion: ^8.0.2
|
framer-motion: ^8.0.2
|
||||||
jotai: ^1.10.0
|
|
||||||
jsdom: ^21.1.0
|
jsdom: ^21.1.0
|
||||||
luxon: ^3.3.0
|
luxon: ^3.3.0
|
||||||
ontime-types: workspace:*
|
ontime-types: workspace:*
|
||||||
@@ -114,7 +113,6 @@ importers:
|
|||||||
csv-stringify: 6.2.3
|
csv-stringify: 6.2.3
|
||||||
deepmerge: 4.3.0
|
deepmerge: 4.3.0
|
||||||
framer-motion: 8.4.3_biqbaboplfbrettd7655fr4n2y
|
framer-motion: 8.4.3_biqbaboplfbrettd7655fr4n2y
|
||||||
jotai: 1.13.0_react@18.2.0
|
|
||||||
luxon: 3.3.0
|
luxon: 3.3.0
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-beautiful-dnd: 13.1.1_biqbaboplfbrettd7655fr4n2y
|
react-beautiful-dnd: 13.1.1_biqbaboplfbrettd7655fr4n2y
|
||||||
@@ -6219,49 +6217,6 @@ packages:
|
|||||||
'@sideway/pinpoint': 2.0.0
|
'@sideway/pinpoint': 2.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/jotai/1.13.0_react@18.2.0:
|
|
||||||
resolution: {integrity: sha512-SPTO46Jw1aJTp2i18tiKgk++nKNKJTMNw4qYFy9QRTydceWvD2Eys+YgB7cb1S6pzbM9i0Ik/y7Yl7IH8k9Lig==}
|
|
||||||
engines: {node: '>=12.20.0'}
|
|
||||||
peerDependencies:
|
|
||||||
'@babel/core': '*'
|
|
||||||
'@babel/template': '*'
|
|
||||||
jotai-devtools: '*'
|
|
||||||
jotai-immer: '*'
|
|
||||||
jotai-optics: '*'
|
|
||||||
jotai-redux: '*'
|
|
||||||
jotai-tanstack-query: '*'
|
|
||||||
jotai-urql: '*'
|
|
||||||
jotai-valtio: '*'
|
|
||||||
jotai-xstate: '*'
|
|
||||||
jotai-zustand: '*'
|
|
||||||
react: '>=16.8'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@babel/core':
|
|
||||||
optional: true
|
|
||||||
'@babel/template':
|
|
||||||
optional: true
|
|
||||||
jotai-devtools:
|
|
||||||
optional: true
|
|
||||||
jotai-immer:
|
|
||||||
optional: true
|
|
||||||
jotai-optics:
|
|
||||||
optional: true
|
|
||||||
jotai-redux:
|
|
||||||
optional: true
|
|
||||||
jotai-tanstack-query:
|
|
||||||
optional: true
|
|
||||||
jotai-urql:
|
|
||||||
optional: true
|
|
||||||
jotai-valtio:
|
|
||||||
optional: true
|
|
||||||
jotai-xstate:
|
|
||||||
optional: true
|
|
||||||
jotai-zustand:
|
|
||||||
optional: true
|
|
||||||
dependencies:
|
|
||||||
react: 18.2.0
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/js-sdsl/4.2.0:
|
/js-sdsl/4.2.0:
|
||||||
resolution: {integrity: sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==}
|
resolution: {integrity: sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|||||||
Reference in New Issue
Block a user