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:
Carlos Valente
2023-03-24 08:19:47 +01:00
committed by GitHub
parent e937af62b1
commit bca7ad30b1
44 changed files with 434 additions and 799 deletions
@@ -1,5 +1,6 @@
import { Tooltip } from '@chakra-ui/react';
import { Playback } from 'ontime-types';
import { millisToString } from 'ontime-utils';
import TimerDisplay from '../../../common/components/timer-display/TimerDisplay';
import { setPlayback, useTimer } from '../../../common/hooks/useSocket';
@@ -9,7 +10,6 @@ import { tooltipDelayMid } from '../../../ontimeConfig';
import TapButton from './TapButton';
import style from './PlaybackControl.module.scss';
import { millisToString } from 'ontime-utils';
interface PlaybackTimerProps {
playback: Playback;
@@ -17,20 +17,20 @@ interface PlaybackTimerProps {
export default function PlaybackTimer(props: PlaybackTimerProps) {
const { playback } = props;
const data = useTimer();
const timer = useTimer();
// TODO: checkout typescript in utilities
const started = millisToString(data.timer.startedAt);
const finish = millisToString(data.timer.expectedFinish);
const started = millisToString(timer.startedAt);
const finish = millisToString(timer.expectedFinish);
const isRolling = playback === Playback.Roll;
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 isOvertime = data.timer.current !== null && data.timer.current < 0;
const hasAddedTime = Boolean(data.timer.addedTime);
const isOvertime = timer.current !== null && timer.current < 0;
const hasAddedTime = Boolean(timer.addedTime);
const rollLabel = isRolling ? 'Roll mode active' : '';
const addedTimeLabel = hasAddedTime ? `Added ${millisToMinutes(data.timer.addedTime)} minutes` : '';
const addedTimeLabel = hasAddedTime ? `Added ${millisToMinutes(timer.addedTime)} minutes` : '';
return (
<div className={style.timeContainer}>
@@ -44,7 +44,7 @@ export default function PlaybackTimer(props: PlaybackTimerProps) {
</Tooltip>
</div>
<div className={style.timer}>
<TimerDisplay time={isWaiting ? data.timer.secondaryTimer : data.timer.current} />
<TimerDisplay time={isWaiting ? timer.secondaryTimer : timer.current} />
</div>
{isWaiting ? (
<div className={style.roll}>
+1 -3
View File
@@ -3,9 +3,9 @@ import { Box, useDisclosure } from '@chakra-ui/react';
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
import UploadModal from '../../common/components/upload-modal/UploadModal';
import ModalManager from '../modals/ModalManager';
import MenuBar from '../menu/MenuBar';
import IntegrationModal from '../modals/integration-modal/IntegrationModal';
import ModalManager from '../modals/ModalManager';
import styles from './Editor.module.scss';
@@ -17,9 +17,7 @@ const EventEditor = lazy(() => import('../../features/event-editor/EventEditorEx
export default function Editor() {
const { isOpen: isSettingsOpen, onOpen: onSettingsOpen, onClose: onSettingsClose } = useDisclosure();
const { isOpen: isUploadModalOpen, onOpen: onUploadModalOpen, onClose: onUploadModalClose } = useDisclosure();
const {
isOpen: isIntegrationModalOpen,
onOpen: onIntegrationModalOpen,
@@ -1,17 +1,16 @@
import { useCallback, useEffect, useState } from 'react';
import { Button, Select, Switch } from '@chakra-ui/react';
import { IoBan } from '@react-icons/all-files/io5/IoBan';
import { useAtom } from 'jotai';
import { EndAction, OntimeEvent, TimerType } from 'ontime-types';
import { millisToString } from 'ontime-utils';
import { editorEventId } from '../../common/atoms/LocalEventSettings';
import CopyTag from '../../common/components/copy-tag/CopyTag';
import ColourInput from '../../common/components/input/colour-input/ColourInput';
import TextInput from '../../common/components/input/text-input/TextInput';
import TimeInput from '../../common/components/input/time-input/TimeInput';
import { useEventAction } from '../../common/hooks/useEventAction';
import useRundown from '../../common/hooks-query/useRundown';
import { useEventEditorStore } from '../../common/stores/eventEditor';
import { useEmitLog } from '../../common/stores/logger';
import { millisToMinutes } from '../../common/utils/dateConfig';
import getDelayTo from '../../common/utils/getDelayTo';
@@ -23,7 +22,7 @@ export type EventEditorSubmitActions = keyof OntimeEvent | 'durationOverride';
// Todo: add previous end to TimeInput fields
export default function EventEditor() {
const [openId] = useAtom(editorEventId);
const { openId } = useEventEditorStore();
const { data } = useRundown();
const { emitWarning, emitError } = useEmitLog();
const { updateEvent } = useEventAction();
@@ -226,7 +225,14 @@ export default function EventEditor() {
</div>
<div className={`${style.column} ${style.fullHeight}`}>
<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>
@@ -1,8 +1,8 @@
import { Box, IconButton } from '@chakra-ui/react';
import { FiX } from '@react-icons/all-files/fi/FiX';
import { editorEventId } from '../../common/atoms/LocalEventSettings';
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
import { useAtom } from 'jotai';
import { useEventEditorStore } from '../../common/stores/eventEditor';
import EventEditor from './EventEditor';
@@ -17,7 +17,7 @@ const closeBtnStyle = {
};
export default function InfoExport() {
const [openId, setOpenId] = useAtom(editorEventId);
const { openId, removeOpenEvent } = useEventEditorStore();
return (
<Box className={`${style.eventEditor} ${!openId ? style.noEvent : ''}`}>
@@ -25,14 +25,8 @@ export default function InfoExport() {
<div className={style.eventEditorLayout}>
<EventEditor />
<div className={style.header}>
<IconButton
aria-label='Close Menu'
icon={<FiX />}
onClick={() => setOpenId(null)}
{...closeBtnStyle}
/>
<IconButton aria-label='Close Menu' icon={<FiX />} onClick={removeOpenEvent} {...closeBtnStyle} />
</div>
</div>
</ErrorBoundary>
</Box>
@@ -14,16 +14,15 @@ import {
} from '@chakra-ui/react';
import { FiEye } from '@react-icons/all-files/fi/FiEye';
import { FiX } from '@react-icons/all-files/fi/FiX';
import { useAtom } from 'jotai';
import { useEmitLog } from '@/common/stores/logger';
import { version } from '../../../package.json';
import { getLatestVersion, postSettings } from '../../common/api/ontimeApi';
import { eventSettingsAtom } from '../../common/atoms/LocalEventSettings';
import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn';
import useSettings from '../../common/hooks-query/useSettings';
import { ontimePlaceholderSettings } from '../../common/models/OntimeSettings';
import { useLocalEvent } from '../../common/stores/localEvent';
import { inputProps } from './modalHelper';
import SubmitContainer from './SubmitContainer';
@@ -38,7 +37,7 @@ export default function AppSettingsModal() {
const [submitting, setSubmitting] = useState(false);
const [hidePin, setHidePin] = useState(true);
const [eventSettings, saveEventSettings] = useAtom(eventSettingsAtom);
const { eventSettings, setLocalEventSettings } = useLocalEvent();
const [formSettings, setFormSettings] = useState(eventSettings);
const [updateMessage, setUpdateMessage] = useState(<a>Using ontime version: {version}</a>);
@@ -69,7 +68,7 @@ export default function AppSettingsModal() {
const hasChanged = !isEqual(formSettings, eventSettings);
if (hasChanged) {
saveEventSettings(formSettings);
setLocalEventSettings(formSettings);
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>
);
}
+6 -5
View File
@@ -2,14 +2,13 @@ import { createRef, Fragment, useCallback, useContext, useEffect } from 'react';
import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd';
import { Button } from '@chakra-ui/react';
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
import { useAtomValue } from 'jotai';
import { OntimeRundown, SupportedEvent } from 'ontime-types';
import { defaultPublicAtom, showQuickEntryAtom, startTimeIsLastEndAtom } from '../../common/atoms/LocalEventSettings';
import Empty from '../../common/components/state/Empty';
import { CursorContext } from '../../common/context/CursorContext';
import { useEventAction } from '../../common/hooks/useEventAction';
import { useRundownEditor } from '../../common/hooks/useSocket';
import { useLocalEvent } from '../../common/stores/localEvent';
import { cloneEvent } from '../../common/utils/eventsManager';
import QuickAddBlock from './quick-add-block/QuickAddBlock';
@@ -25,11 +24,13 @@ export default function Rundown(props: RundownProps) {
const { entries } = props;
const data = useRundownEditor();
const { cursor, moveCursorUp, moveCursorDown, moveCursorTo, isCursorLocked } = useContext(CursorContext);
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
const defaultPublic = useAtomValue(defaultPublicAtom);
const { addEvent, reorderEvent } = useEventAction();
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(
(type: SupportedEvent | 'clone', cursor: number) => {
@@ -1,10 +1,10 @@
import { useCallback, useContext } from 'react';
import { useAtom, useAtomValue } from 'jotai';
import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types';
import { defaultPublicAtom, editorEventId, startTimeIsLastEndAtom } from '../../common/atoms/LocalEventSettings';
import { CursorContext } from '../../common/context/CursorContext';
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 { cloneEvent } from '../../common/utils/eventsManager';
import { calculateDuration } from '../../common/utils/timesManager';
@@ -32,11 +32,13 @@ interface RundownEntryProps {
export default function RundownEntry(props: RundownEntryProps) {
const { index, eventIndex, data, selected, hasCursor, next, delay, previousEnd, previousEventId, playback } = props;
const { emitError } = useEmitLog();
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
const defaultPublic = useAtomValue(defaultPublicAtom);
const { openId, removeOpenEvent } = useEventEditorStore();
const { addEvent, updateEvent, deleteEvent } = useEventAction();
const { moveCursorTo } = useContext(CursorContext);
const [openId, setOpenId] = useAtom(editorEventId);
const { eventSettings } = useLocalEvent();
const defaultPublic = eventSettings.defaultPublic;
const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd;
// Create / delete new events
type FieldValue = {
@@ -71,7 +73,7 @@ export default function RundownEntry(props: RundownEntryProps) {
}
case 'delete': {
if (openId === data.id) {
setOpenId(null);
removeOpenEvent();
}
deleteEvent(data.id);
break;
@@ -122,7 +124,7 @@ export default function RundownEntry(props: RundownEntryProps) {
moveCursorTo,
openId,
previousEventId,
setOpenId,
removeOpenEvent,
startTimeIsLastEnd,
updateEvent,
],
@@ -10,13 +10,12 @@ import { IoReload } from '@react-icons/all-files/io5/IoReload';
import { IoRemoveCircle } from '@react-icons/all-files/io5/IoRemoveCircle';
import { IoRemoveCircleOutline } from '@react-icons/all-files/io5/IoRemoveCircleOutline';
import { IoReorderTwo } from '@react-icons/all-files/io5/IoReorderTwo';
import { useAtom } from 'jotai';
import { Playback } from 'ontime-types';
import { editorEventId } from '../../../common/atoms/LocalEventSettings';
import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn';
import { useEventAction } from '../../../common/hooks/useEventAction';
import { setEventPlayback } from '../../../common/hooks/useSocket';
import { useEventEditorStore } from '../../../common/stores/eventEditor';
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
import { tooltipDelayMid } from '../../../ontimeConfig';
import { EventItemActions } from '../RundownEntry';
@@ -78,7 +77,7 @@ export default function EventBlock(props: EventBlockProps) {
actionHandler,
} = props;
const [openId, setOpenId] = useAtom(editorEventId);
const { openId, setOpenEvent, removeOpenEvent } = useEventEditorStore();
const { updateEvent } = useEventAction();
const [blockTitle, setBlockTitle] = useState<string>(title || '');
const onFocusRef = useRef<null | HTMLSpanElement>(null);
@@ -111,6 +110,14 @@ export default function EventBlock(props: EventBlockProps) {
[title, updateEvent, eventId],
);
const toggleOpenEvent = useCallback(() => {
if (openId === eventId) {
removeOpenEvent();
} else {
setOpenEvent(eventId);
}
}, [eventId, openId, removeOpenEvent, setOpenEvent]);
const eventIsPlaying = selected && playback === Playback.Play;
const playBtnStyles = { _hover: {} };
if (!skip && eventIsPlaying) {
@@ -221,7 +228,7 @@ export default function EventBlock(props: EventBlockProps) {
variant='ontime-subtle-white'
size='sm'
icon={<IoOptions />}
clickHandler={() => setOpenId((prev) => (prev === eventId ? null : eventId))}
clickHandler={toggleOpenEvent}
tooltip='Event options'
aria-label='Event options'
tabIndex={-1}
@@ -11,25 +11,16 @@ interface EventBlockProgressBarProps {
export default function EventBlockProgressBar(props: EventBlockProgressBarProps) {
const { playback } = props;
const { data: timer } = useTimer();
const timer = useTimer();
const now = Math.floor(Math.max((timer?.current ?? 1) / 1000, 0));
const complete = (timer?.duration ?? 1) / 1000;
const elapsed = clamp(100 - (now * 100) / complete, 0, 100);
const progress = `${elapsed}%`;
if ((timer?.current ?? 0) < 0) {
return (
<div
className={`${style.progressBar} ${style.overtime}`}
style={{ width: '100%' }}
/>
);
return <div className={`${style.progressBar} ${style.overtime}`} style={{ width: '100%' }} />;
}
return (
<div
className={`${style.progressBar} ${playback ? style[playback] : ''}`}
style={{ width: progress }}
/>
);
return <div className={`${style.progressBar} ${playback ? style[playback] : ''}`} style={{ width: progress }} />;
}
@@ -1,10 +1,9 @@
import { useCallback, useRef } from 'react';
import { Button, Checkbox, Tooltip } from '@chakra-ui/react';
import { useAtomValue } from 'jotai';
import { SupportedEvent } from 'ontime-types';
import { defaultPublicAtom, startTimeIsLastEndAtom } from '../../../common/atoms/LocalEventSettings';
import { useEventAction } from '../../../common/hooks/useEventAction';
import { useLocalEvent } from '../../../common/stores/localEvent';
import { useEmitLog } from '../../../common/stores/logger';
import { tooltipDelayMid } from '../../../ontimeConfig';
@@ -22,11 +21,14 @@ export default function QuickAddBlock(props: QuickAddBlockProps) {
const { showKbd, eventId, previousEventId, disableAddDelay = true, disableAddBlock } = props;
const { addEvent } = useEventAction();
const { emitError } = useEmitLog();
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
const defaultPublic = useAtomValue(defaultPublicAtom);
const doStartTime = useRef<HTMLInputElement | null>(null);
const doPublic = useRef<HTMLInputElement | null>(null);
const { eventSettings } = useLocalEvent();
const defaultPublic = eventSettings.defaultPublic;
const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd;
const handleCreateEvent = useCallback(
(eventType: SupportedEvent) => {
switch (eventType) {
@@ -20,17 +20,16 @@ import PlaybackIcon from './tableElements/PlaybackIcon';
import style from './Table.module.scss';
export default function TableHeader({ handleCSVExport, featureData }) {
const { followSelected, showSettings, toggleTheme, toggleSettings, toggleFollow } =
useContext(TableSettingsContext);
const { data: timer } = useTimer();
const { followSelected, showSettings, toggleTheme, toggleSettings, toggleFollow } = useContext(TableSettingsContext);
const timer = useTimer();
const { isFullScreen, toggleFullScreen } = useFullscreen();
const { data: event } = useEventData();
const selected = !featureData.numEvents
? 'No events'
: `Event ${featureData.selectedEventIndex != null ? featureData.selectedEventIndex + 1 : '-'}/${
featureData.numEvents ? featureData.numEvents : '-'
}`;
featureData.numEvents ? featureData.numEvents : '-'
}`;
// prepare presentation variables
const isOvertime = timer.current < 0;
+17 -18
View File
@@ -15,8 +15,8 @@ import style from './Table.module.scss';
export default function TableWrapper() {
const { data: rundown } = useRundown();
const { data: userFields } = useUserFields();
const { data: featureData } = useCuesheet();
const { updateEvent } = useEventAction();
const featureData = useCuesheet();
const { theme } = useContext(TableSettingsContext);
// Set window title
@@ -30,11 +30,11 @@ export default function TableWrapper() {
return;
}
// check if value is the same
const event = rundown[rowIndex];
if (event == null) {
return;
}
// check if value is the same
const event = rundown[rowIndex];
if (event == null) {
return;
}
if (event[accessor] === payload) {
return;
@@ -52,13 +52,15 @@ export default function TableWrapper() {
[accessor]: cleanVal,
};
// submit
try {
await updateEvent(mutationObject);
} catch (error) {
console.error(error);
}
}, [updateEvent, rundown]);
// submit
try {
await updateEvent(mutationObject);
} catch (error) {
console.error(error);
}
},
[updateEvent, rundown],
);
const exportHandler = useCallback(
(headerData) => {
@@ -75,17 +77,14 @@ export default function TableWrapper() {
document.body.appendChild(link);
link.click();
},
[rundown, userFields]
[rundown, userFields],
);
if (typeof rundown === 'undefined' || typeof userFields === 'undefined') {
return <span>loading...</span>;
}
return (
<div
className={theme === 'dark' ? style.tableWrapper__dark : style.tableWrapper}
data-testid="cuesheet"
>
<div className={theme === 'dark' ? style.tableWrapper__dark : style.tableWrapper} data-testid='cuesheet'>
<TableHeader handleCSVExport={exportHandler} featureData={featureData} />
<OntimeTable
tableData={rundown}
@@ -1,13 +1,18 @@
import { ReactNode, useMemo } from 'react';
import { Playback } from 'ontime-types';
import { Playback, TitleBlock } from 'ontime-types';
import useEventData from '../../common/hooks-query/useEventData';
import useRundown from '../../common/hooks-query/useRundown';
import useViewSettings from '../../common/hooks-query/useViewSettings';
import { useRuntimeStore } from '../../common/stores/runtime';
import { useViewOptionsStore } from '../../common/stores/viewOptions';
export type TitleManager = TitleBlock & { showNow: boolean; showNext: boolean };
const withData = (Component: ReactNode) => {
return (props) => {
// persisted app state
const { mirror: isMirrored } = useViewOptionsStore();
// HTTP API data
const { data: eventsData } = useRundown();
@@ -41,7 +46,7 @@ const withData = (Component: ReactNode) => {
let showNext = true;
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 ***/
@@ -56,7 +61,7 @@ const withData = (Component: ReactNode) => {
let showPublicNext = true;
if (!titlesPublic.titleNext && !titlesPublic.subtitleNext && !titlesPublic.presenterNext) showPublicNext = false;
const publicTitleManager = {
const publicTitleManager: TitleManager = {
...titlesPublic,
showNow: showPublicNow,
showNext: showPublicNext,
@@ -85,6 +90,7 @@ const withData = (Component: ReactNode) => {
return (
<Component
{...props}
isMirrored={isMirrored}
pres={timerMessage}
publ={publicMessage}
lower={lowerMessage}
@@ -1,11 +1,9 @@
import { useEffect } from 'react';
import QRCode from 'react-qr-code';
import { AnimatePresence, motion } from 'framer-motion';
import { useAtom } from 'jotai';
import PropTypes from 'prop-types';
import { EventData, Message, OntimeEvent, ViewSettings } from 'ontime-types';
import { overrideStylesURL } from '../../../common/api/apiConstants';
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
import ProgressBar from '../../../common/components/progress-bar/ProgressBar';
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 TitleCard from '../../../common/components/title-card/TitleCard';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { TimeManagerType } from '../../../common/models/TimeManager.type';
import { formatDisplay, millisToSeconds } from '../../../common/utils/dateConfig';
import { getEventsWithDelay } from '../../../common/utils/eventsManager';
import { formatTime } from '../../../common/utils/time';
import { titleVariants } from '../common/animation';
import { TitleManager } from '../ViewWrapper';
import './Backstage.scss';
@@ -25,21 +25,20 @@ const formatOptions = {
format: 'hh:mm:ss a',
};
Backstage.propTypes = {
publ: PropTypes.object,
title: PropTypes.object,
time: PropTypes.object,
backstageEvents: PropTypes.array,
selectedId: PropTypes.string,
general: PropTypes.object,
viewSettings: PropTypes.object,
};
interface BackstageProps {
isMirrored: boolean;
publ: Message;
title: TitleManager;
time: TimeManagerType;
backstageEvents: OntimeEvent[];
selectedId: string | null;
general: EventData;
viewSettings: ViewSettings;
}
// @ts-expect-error unable to type just yet
export default function Backstage(props) {
const { publ, title, time, backstageEvents, selectedId, general, viewSettings } = props;
export default function Backstage(props: BackstageProps) {
const { isMirrored, publ, title, time, backstageEvents, selectedId, general, viewSettings } = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const [isMirrored] = useAtom(mirrorViewersAtom);
// Set window title
useEffect(() => {
@@ -1,10 +1,8 @@
import { useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useAtom } from 'jotai';
import { ViewSettings } from 'ontime-types';
import { overrideStylesURL } from '../../../common/api/apiConstants';
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { TimeManagerType } from '../../../common/models/TimeManager.type';
@@ -14,6 +12,7 @@ import { formatTime } from '../../../common/utils/time';
import './Clock.scss';
interface ClockProps {
isMirrored: boolean;
time: TimeManagerType;
viewSettings: ViewSettings;
}
@@ -24,10 +23,9 @@ const formatOptions = {
};
export default function Clock(props: ClockProps) {
const { time, viewSettings } = props;
const { isMirrored, time, viewSettings } = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const [searchParams] = useSearchParams();
const [isMirrored] = useAtom(mirrorViewersAtom);
useEffect(() => {
document.title = 'ontime - Clock';
@@ -1,13 +1,11 @@
import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useAtom } from 'jotai';
import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types';
import PropTypes from 'prop-types';
import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent, ViewSettings } from 'ontime-types';
import { overrideStylesURL } from '../../../common/api/apiConstants';
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { TimeManagerType } from '../../../common/models/TimeManager.type';
import { formatDisplay, millisToSeconds } from '../../../common/utils/dateConfig';
import getDelayTo from '../../../common/utils/getDelayTo';
import { formatTime } from '../../../common/utils/time';
@@ -27,19 +25,18 @@ const formatOptionsFinished = {
format: 'hh:mm a',
};
Countdown.propTypes = {
backstageEvents: PropTypes.array,
time: PropTypes.object,
selectedId: PropTypes.string,
viewSettings: PropTypes.object,
};
interface CountdownProps {
isMirrored: boolean;
backstageEvents: OntimeEvent[];
time: TimeManagerType;
selectedId: string | null;
viewSettings: ViewSettings;
}
// @ts-expect-error we are unable to type this just yet
export default function Countdown(props) {
const { backstageEvents, time, selectedId, viewSettings } = props;
export default function Countdown(props: CountdownProps) {
const { isMirrored, backstageEvents, time, selectedId, viewSettings } = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const [searchParams] = useSearchParams();
const [isMirrored] = useAtom(mirrorViewersAtom);
const [follow, setFollow] = useState<OntimeEvent | null>(null);
const [runningTimer, setRunningTimer] = useState(0);
@@ -97,20 +94,15 @@ export default function Countdown(props) {
const delayedTimerStyles = delay > 0 ? 'aux-timers__value--delayed' : '';
const clock = formatTime(time.clock, formatOptions);
const startTime =
follow === null
? '...'
: formatTime(follow.timeStart + delay, formatOptions);
const endTime =
follow === null
? '...'
: formatTime(follow.timeEnd + delay, formatOptions);
const formattedTimer = runningMessage === TimerMessage.ended
? formatTime(runningTimer, formatOptionsFinished)
: formatDisplay(
isSelected ? millisToSeconds(runningTimer) : millisToSeconds(runningTimer + delay),
isSelected || time.waiting,
);
const startTime = follow === null ? '...' : formatTime(follow.timeStart + delay, formatOptions);
const endTime = follow === null ? '...' : formatTime(follow.timeEnd + delay, formatOptions);
const formattedTimer =
runningMessage === TimerMessage.ended
? formatTime(runningTimer, formatOptionsFinished)
: formatDisplay(
isSelected ? millisToSeconds(runningTimer) : millisToSeconds(runningTimer + delay),
isSelected || time.waiting,
);
return (
<div className={`countdown ${isMirrored ? 'mirror' : ''}`} data-testid='countdown-view'>
@@ -119,7 +111,6 @@ export default function Countdown(props) {
<CountdownSelect events={backstageEvents} />
) : (
<div className='countdown-container' data-testid='countdown-event'>
<div className='clock-container'>
<div className='label'>Time Now</div>
<div className='time'>{clock}</div>
@@ -127,11 +118,7 @@ export default function Countdown(props) {
<div className='status'>{runningMessage}</div>
<span
className={`timer ${standby ? 'timer--paused' : ''} ${
isRunningFinished ? 'timer--finished' : ''
}`}
>
<span className={`timer ${standby ? 'timer--paused' : ''} ${isRunningFinished ? 'timer--finished' : ''}`}>
{formattedTimer}
</span>
<div className='title'>{follow?.title || 'Untitled Event'}</div>
@@ -139,18 +126,13 @@ export default function Countdown(props) {
<div className='timer-group'>
<div className='aux-timers'>
<div className='aux-timers__label'>Start Time</div>
<span className={`aux-timers__value ${delayedTimerStyles}`}>
{startTime}
</span>
<span className={`aux-timers__value ${delayedTimerStyles}`}>{startTime}</span>
</div>
<div className='aux-timers'>
<div className='aux-timers__label'>End Time</div>
<span className={`aux-timers__value ${delayedTimerStyles}`}>
{endTime}
</span>
<span className={`aux-timers__value ${delayedTimerStyles}`}>{endTime}</span>
</div>
</div>
</div>
)}
</div>
@@ -1,10 +1,8 @@
import { useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useAtom } from 'jotai';
import { EventData, Message, Playback, TimerType, ViewSettings } from 'ontime-types';
import { overrideStylesURL } from '../../../common/api/apiConstants';
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { TimeManagerType } from '../../../common/models/TimeManager.type';
@@ -14,6 +12,7 @@ import { formatTimerDisplay, getTimerByType } from '../common/viewerUtils';
import './MinimalTimer.scss';
interface MinimalTimerProps {
isMirrored: boolean;
pres: Message;
time: TimeManagerType;
viewSettings: ViewSettings;
@@ -21,10 +20,9 @@ interface 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 [searchParams] = useSearchParams();
const [isMirrored] = useAtom(mirrorViewersAtom);
useEffect(() => {
document.title = 'ontime - Minimal Timer';
@@ -1,19 +1,19 @@
import { useEffect } from 'react';
import QRCode from 'react-qr-code';
import { AnimatePresence, motion } from 'framer-motion';
import { useAtom } from 'jotai';
import PropTypes from 'prop-types';
import { EventData, Message, OntimeEvent, ViewSettings } from 'ontime-types';
import { overrideStylesURL } from '../../../common/api/apiConstants';
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
import Schedule from '../../../common/components/schedule/Schedule';
import { ScheduleProvider } from '../../../common/components/schedule/ScheduleContext';
import ScheduleNav from '../../../common/components/schedule/ScheduleNav';
import TitleCard from '../../../common/components/title-card/TitleCard';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { TimeManagerType } from '../../../common/models/TimeManager.type';
import { formatTime } from '../../../common/utils/time';
import { titleVariants } from '../common/animation';
import { TitleManager } from '../ViewWrapper';
import './Public.scss';
@@ -22,21 +22,20 @@ const formatOptions = {
format: 'hh:mm:ss a',
};
Public.propTypes = {
publ: PropTypes.object,
publicTitle: PropTypes.object,
time: PropTypes.object,
events: PropTypes.array,
publicSelectedId: PropTypes.string,
general: PropTypes.object,
viewSettings: PropTypes.object,
};
interface BackstageProps {
isMirrored: boolean;
publ: Message;
publicTitle: TitleManager;
time: TimeManagerType;
events: OntimeEvent[];
publicSelectedId: string | null;
general: EventData;
viewSettings: ViewSettings;
}
// @ts-expect-error unable to type just yet
export default function Public(props) {
const { publ, publicTitle, time, events, publicSelectedId, general, viewSettings } = props;
export default function Public(props: BackstageProps) {
const { isMirrored, publ, publicTitle, time, events, publicSelectedId, general, viewSettings } = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const [isMirrored] = useAtom(mirrorViewersAtom);
useEffect(() => {
document.title = 'ontime - Public Screen';
@@ -1,10 +1,9 @@
import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useAtom } from 'jotai';
import { millisToString } from 'ontime-utils';
import PropTypes from 'prop-types';
import { overrideStylesURL } from '../../../common/api/apiConstants';
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
import useFitText from '../../../common/hooks/useFitText';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
@@ -13,7 +12,6 @@ import { formatEventList, getEventsWithDelay, trimEventlist } from '../../../com
import { formatTime } from '../../../common/utils/time';
import './StudioClock.scss';
import { millisToString } from 'ontime-utils';
const formatOptions = {
showSeconds: false,
@@ -21,6 +19,7 @@ const formatOptions = {
};
StudioClock.propTypes = {
isMirrored: PropTypes.bool,
title: PropTypes.object,
time: PropTypes.object,
backstageEvents: PropTypes.array,
@@ -31,14 +30,13 @@ StudioClock.propTypes = {
};
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)
useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const { fontSize: titleFontSize, ref: titleRef } = useFitText({ maxFontSize: 500 });
const [schedule, setSchedule] = useState([]);
const [isMirrored] = useAtom(mirrorViewersAtom);
const activeIndicators = [...Array(12).keys()];
const secondsIndicators = [...Array(60).keys()];
@@ -1,17 +1,16 @@
import { useEffect } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import { useAtom } from 'jotai';
import { Playback, TimerType } from 'ontime-types';
import PropTypes from 'prop-types';
import { EventData, Message, Playback, TimerType, ViewSettings } from 'ontime-types';
import { overrideStylesURL } from '../../../common/api/apiConstants';
import { mirrorViewersAtom } from '../../../common/atoms/ViewerSettings';
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
import ProgressBar from '../../../common/components/progress-bar/ProgressBar';
import TitleCard from '../../../common/components/title-card/TitleCard';
import { useRuntimeStylesheet } from '../../../common/hooks/useRuntimeStylesheet';
import { TimeManagerType } from '../../../common/models/TimeManager.type';
import { formatTime } from '../../../common/utils/time';
import { formatTimerDisplay, getTimerByType } from '../common/viewerUtils';
import { TitleManager } from '../ViewWrapper';
import './Timer.scss';
@@ -36,19 +35,18 @@ const titleVariants = {
},
};
Timer.propTypes = {
general: PropTypes.object,
pres: PropTypes.object,
title: PropTypes.object,
time: PropTypes.object,
viewSettings: PropTypes.object,
};
interface TimerProps {
isMirrored: boolean;
general: EventData;
pres: Message;
title: TitleManager;
time: TimeManagerType;
viewSettings: ViewSettings;
}
// @ts-expect-error unable to type just yet
export default function Timer(props) {
const { general, pres, title, time, viewSettings } = props;
export default function Timer(props: TimerProps) {
const { isMirrored, general, pres, title, time, viewSettings } = props;
const { shouldRender } = useRuntimeStylesheet(viewSettings?.overrideStyles && overrideStylesURL);
const [isMirrored] = useAtom(mirrorViewersAtom);
useEffect(() => {
document.title = 'ontime - Timer';