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
View File
@@ -141,3 +141,4 @@ jobs:
file: ./Dockerfile
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ontime:${{ env.RELEASE_VERSION }} , ${{ secrets.DOCKERHUB_USERNAME }}/ontime:latest
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6
-1
View File
@@ -20,7 +20,6 @@
"csv-stringify": "^6.2.3",
"deepmerge": "^4.3.0",
"framer-motion": "^8.0.2",
"jotai": "^1.10.0",
"luxon": "^3.3.0",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
+6 -5
View File
@@ -40,6 +40,7 @@ export default function AppRouter() {
// navigate if is alias route
useEffect(() => {
if (!data) return;
for (const d of data) {
if (`/${d.alias}` === location.pathname && d.enabled) {
navigate(`/${d.pathAndParams}`);
@@ -48,9 +49,9 @@ export default function AppRouter() {
}
}, [data, location, navigate]);
return(
return (
<Routes>
<Route path='/' element={<Navigate to="/timer" /> } />
<Route path='/' element={<Navigate to='/timer' />} />
<Route path='/speaker' element={<STimer />} />
<Route path='/presenter' element={<STimer />} />
<Route path='/stage' element={<STimer />} />
@@ -111,8 +112,8 @@ export default function AppRouter() {
</FeatureWrapper>
}
/>
{/* Send to default if nothing found */}
<Route path='*' element={<Navigate to="/timer" /> } />
{/*/!* Send to default if nothing found *!/*/}
<Route path='*' element={<STimer />} />
</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
ref={inputRef}
size={size}
resize='none'
resize={resize}
variant='ontime-filled'
{...textAreaProps}
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 { millisToString } from 'ontime-utils';
@@ -21,9 +21,7 @@ interface TimeInputProps {
}
export default function TimeInput(props: TimeInputProps) {
const {
name, submitHandler, time = 0, delay = 0, placeholder, validationHandler, previousEnd = 0,
} = props;
const { name, submitHandler, time = 0, delay = 0, placeholder, validationHandler, previousEnd = 0 } = props;
const { emitError } = useEmitLog();
const inputRef = useRef<HTMLInputElement | null>(null);
const [value, setValue] = useState('');
@@ -51,74 +49,90 @@ export default function TimeInput(props: TimeInputProps) {
* @description Submit handler
* @param {string} newValue
*/
const handleSubmit = useCallback((newValue: string) => {
// Check if there is anything there
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;
const handleSubmit = useCallback(
(newValue: string) => {
// Check if there is anything there
if (newValue === '') {
return false;
}
} 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
const originalMillis = time + delay;
let newValMillis = 0;
// check if time is different from before
if (newValMillis === originalMillis) return false;
// 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);
}
// validate with parent
if (!validationHandler(name, newValMillis)) return false;
// Time now and time submittedVal
const originalMillis = time + delay;
// update entry
submitHandler(name, newValMillis);
// check if time is different from before
if (newValMillis === originalMillis) return false;
return true;
}, [delay, name, previousEnd, submitHandler, time, validationHandler]);
// validate with parent
if (!validationHandler(name, newValMillis)) return false;
// update entry
submitHandler(name, newValMillis);
return true;
},
[delay, name, previousEnd, submitHandler, time, validationHandler],
);
/**
* @description Prepare time fields
* @param {string} value string to be parsed
*/
const validateAndSubmit = useCallback((newValue: string) => {
const success = handleSubmit(newValue);
if (success) {
const ms = forgivingStringToMillis(newValue);
setValue(millisToString(ms + delay));
} else {
resetValue();
}
}, [delay, handleSubmit, resetValue]);
const validateAndSubmit = useCallback(
(newValue: string) => {
const success = handleSubmit(newValue);
if (success) {
const ms = forgivingStringToMillis(newValue);
setValue(millisToString(ms + delay));
} else {
resetValue();
}
},
[delay, handleSubmit, resetValue],
);
/**
* @description Handles common keys for submit and cancel
* @param {KeyboardEvent} event
*/
const onKeyDownHandler = useCallback((event:KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
inputRef.current?.blur();
const onKeyDownHandler = useCallback(
(event: KeyboardEvent<HTMLInputElement>) => {
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);
} else if (event.key === 'Tab') {
validateAndSubmit((event.target as HTMLInputElement).value);
}
if (event.key === 'Escape') {
inputRef.current?.blur();
resetValue();
}
}, [resetValue, validateAndSubmit]);
},
[validateAndSubmit],
);
useEffect(() => {
if (time == null) return;
@@ -167,7 +181,7 @@ export default function TimeInput(props: TimeInputProps) {
variant='ontime-filled'
onFocus={handleFocus}
onChange={(event) => setValue(event.target.value)}
onBlur={resetValue}
onBlur={onBlurHandler}
onKeyDown={onKeyDownHandler}
value={value}
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 { IoExpand } from '@react-icons/all-files/io5/IoExpand';
import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
import { useAtom } from 'jotai';
import { navigatorConstants } from '../../../viewerConfig';
import { mirrorViewersAtom } from '../../atoms/ViewerSettings';
import useClickOutside from '../../hooks/useClickOutside';
import useFullscreen from '../../hooks/useFullscreen';
import { useKeyDown } from '../../hooks/useKeyDown';
import { useViewOptionsStore } from '../../stores/viewOptions';
import style from './NavigationMenu.module.scss';
@@ -20,7 +19,7 @@ export default function NavigationMenu() {
const location = useLocation();
const { isFullScreen, toggleFullScreen } = useFullscreen();
const [isMirrored, setMirrored] = useAtom(mirrorViewersAtom);
const { mirror, toggleMirror } = useViewOptionsStore();
const [showButton, setShowButton] = useState(false);
const [showMenu, setShowMenu] = useState(false);
const menuRef = useRef<HTMLDivElement | null>(null);
@@ -49,10 +48,10 @@ export default function NavigationMenu() {
const isKeyEnter = (event: KeyboardEvent<HTMLDivElement>) => event.key === 'Enter';
const handleFullscreen = () => toggleFullScreen();
const handleMirror = () => setMirrored((prev) => !prev);
const handleMirror = () => toggleMirror();
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
onClick={toggleMenu}
aria-label='toggle menu'
@@ -83,7 +82,8 @@ export default function NavigationMenu() {
onClick={handleMirror}
onKeyDown={(event) => {
isKeyEnter(event) && handleMirror();
}}>
}}
>
Flip Screen
<IoSwapVertical />
</div>
@@ -97,12 +97,15 @@ export default function NavigationMenu() {
key={route.url}
to={route.url}
className={`${style.link} ${route.url === location.pathname ? style.current : undefined}`}
tabIndex={0}>
tabIndex={0}
>
{route.label}
<IoArrowUp className={style.linkIcon} />
</Link>
))}
</div>
)}
</div>, document.body);
</div>,
document.body,
);
}
@@ -17,23 +17,9 @@ export default function Schedule({ className }: ScheduleProps) {
}
let selectedState: 'past' | 'now' | 'future' = 'past';
const selectedEvent = paginatedEvents.find((event) => event.id === selectedEventId);
return (
<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) => {
if (event.id === selectedEventId) {
selectedState = 'now';
@@ -17,4 +17,4 @@ export default function useEventData() {
});
return { data, status, isError, refetch };
}
}
@@ -1,7 +1,6 @@
import { useCallback } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios, { AxiosError } from 'axios';
import { useAtomValue } from 'jotai';
import { OntimeRundown, OntimeRundownEntry, SupportedEvent } from 'ontime-types';
import { RUNDOWN_TABLE, RUNDOWN_TABLE_KEY } from '../api/apiConstants';
@@ -14,7 +13,7 @@ import {
requestPutEvent,
requestReorderEvent,
} from '../api/eventsApi';
import { defaultPublicAtom, startTimeIsLastEndAtom } from '../atoms/LocalEventSettings';
import { useLocalEvent } from '../stores/localEvent';
import { useEmitLog } from '../stores/logger';
/**
@@ -23,8 +22,9 @@ import { useEmitLog } from '../stores/logger';
export const useEventAction = () => {
const queryClient = useQueryClient();
const { emitError } = useEmitLog();
const defaultPublic = useAtomValue(defaultPublicAtom);
const startTimeIsLastEnd = useAtomValue(startTimeIsLastEndAtom);
const { eventSettings } = useLocalEvent();
const defaultPublic = eventSettings.defaultPublic;
const startTimeIsLastEnd = eventSettings.startTimeIsLastEnd;
/**
* 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];
};
+5 -2
View File
@@ -77,7 +77,10 @@ export const useInfoPanel = () => {
export const useCuesheet = () => {
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,
});
@@ -92,7 +95,7 @@ export const setEventPlayback = {
export const useTimer = () => {
const featureSelector = (state: RuntimeStore) => ({
timer: state.timer,
...state.timer,
});
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 } };
}),
}));
+1 -1
View File
@@ -19,7 +19,7 @@ export const useLogData = () => useStore(logger);
export const addLog = (log: Log) =>
logger.setState((state) => ({
logs: [...state.logs, log],
logs: [log, ...state.logs],
}));
export const clearLogs = () => logger.setState({ logs: [] });
+1
View File
@@ -16,6 +16,7 @@ export const runtimeStorePlaceholder = {
selectedEventId: null,
duration: null,
timerType: null,
endAction: null,
},
playback: Playback.Stop,
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 { 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';
+13 -33
View File
@@ -1,14 +1,4 @@
const {
app,
BrowserWindow,
Menu,
globalShortcut,
Tray,
dialog,
ipcMain,
shell,
Notification,
} = require('electron');
const { app, BrowserWindow, Menu, globalShortcut, Tray, dialog, ipcMain, shell, Notification } = require('electron');
const path = require('path');
const electronConfig = require('./electron.config');
@@ -40,14 +30,13 @@ let splash;
let tray = null;
(async () => {
// in dev mode, we expect both UI and server to be running
if (!isProduction) {
return
if (!isProduction) {
return;
}
try {
const ontimeServer = require(nodePath)
const ontimeServer = require(nodePath);
const { startDb, startServer, startOSCServer, startIntegrations } = ontimeServer;
await startDb();
@@ -76,9 +65,7 @@ function showNotification(title, text) {
function appShutdown() {
// terminate node service
(async () => {
console.log('asking for shutdown 1', nodePath)
const ontimeServer = require(nodePath)
console.log('asking for shutdown 2', ontimeServer)
const ontimeServer = require(nodePath);
const { shutdown } = ontimeServer;
await shutdown(electronConfig.appIni.shutdownCode);
})();
@@ -129,7 +116,8 @@ function createWindow() {
skipTaskbar: 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({
width: 1920,
@@ -167,15 +155,13 @@ app.whenReady().then(() => {
// (available regardless of whether app is in focus)
// bring focus to window
globalShortcut.register('Alt+1', () => {
bringToFront();
bringToFront();
});
// cheat to schedule process
setTimeout(() => {
// Load page served by node or use React dev run
const clientUrl = isProduction
? electronConfig.reactAppUrl.production
: electronConfig.reactAppUrl.development;
const clientUrl = isProduction ? electronConfig.reactAppUrl.production : electronConfig.reactAppUrl.development;
win.loadURL(clientUrl).then(() => {
win.webContents.setBackgroundThrottling(false);
@@ -212,13 +198,13 @@ app.whenReady().then(() => {
// Define context menu
const { getTrayMenu } = require('./src/menu/trayMenu.js');
const trayMenuTemplate = getTrayMenu(bringToFront, askToQuit)
const trayMenuTemplate = getTrayMenu(bringToFront, askToQuit);
const trayContextMenu = Menu.buildFromTemplate(trayMenuTemplate);
tray.setContextMenu(trayContextMenu);
});
const { getApplicationMenu } = require('./src/menu/applicationMenu.js');
const template = getApplicationMenu(isMac, askToQuit)
const template = getApplicationMenu(isMac, askToQuit);
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
@@ -227,16 +213,10 @@ app.once('will-quit', () => {
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
// Test message
ipcMain.on('reload', () => {
win?.reload();
win?.reload();
});
// Terminate
@@ -258,7 +238,7 @@ ipcMain.on('set-window', (event, arg) => {
win.webContents.openDevTools({ mode: 'detach' });
break;
default:
console.log('Electron unhandled window request', arg)
console.log('Electron unhandled window request', arg);
}
});
-45
View File
@@ -70,7 +70,6 @@ importers:
eslint-plugin-simple-import-sort: ^8.0.0
eslint-plugin-testing-library: ^5.9.1
framer-motion: ^8.0.2
jotai: ^1.10.0
jsdom: ^21.1.0
luxon: ^3.3.0
ontime-types: workspace:*
@@ -114,7 +113,6 @@ importers:
csv-stringify: 6.2.3
deepmerge: 4.3.0
framer-motion: 8.4.3_biqbaboplfbrettd7655fr4n2y
jotai: 1.13.0_react@18.2.0
luxon: 3.3.0
react: 18.2.0
react-beautiful-dnd: 13.1.1_biqbaboplfbrettd7655fr4n2y
@@ -6219,49 +6217,6 @@ packages:
'@sideway/pinpoint': 2.0.0
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:
resolution: {integrity: sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==}
dev: true