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